diff --git a/ring-android/app/src/main/java/cx/ring/adapters/ConversationAdapter.java b/ring-android/app/src/main/java/cx/ring/adapters/ConversationAdapter.java
index 2a088edd0399a58bac0222f5c6892c3efd7b1565..e4d5b7578e1eee2c6d15a5617282a1996f4d8895 100644
--- a/ring-android/app/src/main/java/cx/ring/adapters/ConversationAdapter.java
+++ b/ring-android/app/src/main/java/cx/ring/adapters/ConversationAdapter.java
@@ -94,17 +94,9 @@ public class ConversationAdapter extends RecyclerView.Adapter<ConversationViewHo
             }
             return;
         }
-        int lastPos = mTexts.size();
-        int newItems = list.size() - lastPos;
-        if (lastPos == 0 || newItems < 0) {
-            mTexts.clear();
-            mTexts.addAll(list);
-            notifyDataSetChanged();
-        } else {
-            for (int i = lastPos; i < list.size(); i++)
-                mTexts.add(list.get(i));
-            notifyItemRangeInserted(lastPos, newItems);
-        }
+        mTexts.clear();
+        mTexts.addAll(list);
+        notifyDataSetChanged();
     }
 
     @Override
diff --git a/ring-android/app/src/main/java/cx/ring/client/CallActivity.java b/ring-android/app/src/main/java/cx/ring/client/CallActivity.java
index 3450ae0b85dcef69c720af9b90b7fa5f38e60c4c..e73417ba63e39a2646e40c312dfa65bfbc369a41 100644
--- a/ring-android/app/src/main/java/cx/ring/client/CallActivity.java
+++ b/ring-android/app/src/main/java/cx/ring/client/CallActivity.java
@@ -47,6 +47,7 @@ import javax.inject.Inject;
 import cx.ring.BuildConfig;
 import cx.ring.R;
 import cx.ring.application.RingApplication;
+import cx.ring.facades.ConversationFacade;
 import cx.ring.fragments.CallFragment;
 import cx.ring.model.CallContact;
 import cx.ring.model.Conference;
@@ -69,6 +70,9 @@ public class CallActivity extends AppCompatActivity implements Callbacks, CallFr
     @Inject
     AccountService mAccountService;
 
+    @Inject
+    ConversationFacade mConversationFacade;
+
     private boolean init = false;
     private View mMainView;
 
@@ -240,7 +244,7 @@ public class CallActivity extends AppCompatActivity implements Callbacks, CallFr
                 mProximityManager.startTracking();
 
                 if (mSavedConferenceId != null) {
-                    mDisplayedConference = service.getConference(mSavedConferenceId);
+                    mDisplayedConference = mConversationFacade.getConference(mSavedConferenceId);
                 } else {
                     checkExternalCall();
                 }
@@ -276,7 +280,7 @@ public class CallActivity extends AppCompatActivity implements Callbacks, CallFr
 
     private Pair<Account, Uri> guess(Uri number, String account_id) {
         Account a = mAccountService.getAccount(account_id);
-        Conversation conv = service.findConversationByNumber(number);
+        Conversation conv = mConversationFacade.findOrStartConversationByNumber(number);
 
         // Guess account from number
         if (a == null && number != null)
@@ -324,11 +328,11 @@ public class CallActivity extends AppCompatActivity implements Callbacks, CallFr
             SipCall call = new SipCall(null, g.first.getAccountID(), g.second, SipCall.Direction.OUTGOING);
             call.muteVideo(!hasVideo);
 
-            mDisplayedConference = service.placeCall(call);
+            mDisplayedConference = mConversationFacade.placeCall(call);
         } else if (Intent.ACTION_VIEW.equals(action)) {
             String conf_id = u.getLastPathSegment();
             Log.d(TAG, "conf " + conf_id);
-            mDisplayedConference = service.getConference(conf_id);
+            mDisplayedConference = mConversationFacade.getConference(conf_id);
         }
 
         return false;
diff --git a/ring-android/app/src/main/java/cx/ring/client/ConversationActivity.java b/ring-android/app/src/main/java/cx/ring/client/ConversationActivity.java
index e45e83582ebd6b697ff29b354e7c6c4fa7583381..56977f7e51d13de4cd3d92f845428255be2ab10e 100644
--- a/ring-android/app/src/main/java/cx/ring/client/ConversationActivity.java
+++ b/ring-android/app/src/main/java/cx/ring/client/ConversationActivity.java
@@ -33,11 +33,13 @@ import android.support.v7.app.AppCompatActivity;
 import android.support.v7.widget.Toolbar;
 import android.util.Log;
 
+import javax.inject.Inject;
+
 import butterknife.BindView;
 import butterknife.ButterKnife;
 import cx.ring.R;
+import cx.ring.facades.ConversationFacade;
 import cx.ring.fragments.ConversationFragment;
-import cx.ring.model.Uri;
 import cx.ring.service.IDRingService;
 import cx.ring.service.LocalService;
 
@@ -46,12 +48,16 @@ public class ConversationActivity extends AppCompatActivity implements LocalServ
     @BindView(R.id.main_toolbar)
     Toolbar mToolbar;
 
+    @Inject
+    ConversationFacade mConversationFacade;
+
     private static final String TAG = ConversationActivity.class.getSimpleName();
     static final long REFRESH_INTERVAL_MS = 30 * 1000;
 
     private boolean mBound = false;
     private LocalService mService = null;
     private final Handler mRefreshTaskHandler = new Handler();
+
     private ConversationFragment mConversationFragment;
 
     @Override
@@ -137,7 +143,7 @@ public class ConversationActivity extends AppCompatActivity implements LocalServ
         switch (requestCode) {
             case ConversationFragment.REQ_ADD_CONTACT:
                 if (mService != null) {
-                    mService.refreshConversations();
+                    mConversationFacade.refreshConversations();
                 }
                 break;
         }
diff --git a/ring-android/app/src/main/java/cx/ring/dependencyinjection/RingInjectionComponent.java b/ring-android/app/src/main/java/cx/ring/dependencyinjection/RingInjectionComponent.java
index 5d76ce51d0b557a2190f4e0fd67bc0fa1f31c16c..2c1737123c98de382c99a98cec5e92ccb103d7fd 100755
--- a/ring-android/app/src/main/java/cx/ring/dependencyinjection/RingInjectionComponent.java
+++ b/ring-android/app/src/main/java/cx/ring/dependencyinjection/RingInjectionComponent.java
@@ -51,6 +51,7 @@ import cx.ring.services.AccountService;
 import cx.ring.services.CallService;
 import cx.ring.services.ConferenceService;
 import cx.ring.services.ContactServiceImpl;
+import cx.ring.facades.ConversationFacade;
 import cx.ring.services.DaemonService;
 import cx.ring.services.DeviceRuntimeServiceImpl;
 import cx.ring.services.HardwareService;
@@ -135,6 +136,8 @@ public interface RingInjectionComponent {
 
     void inject(NotificationServiceImpl service);
 
+    void inject(ConversationFacade service);
+
     void inject(BootReceiver receiver);
 
     void inject(AboutPresenter presenter);
diff --git a/ring-android/app/src/main/java/cx/ring/dependencyinjection/ServiceInjectionModule.java b/ring-android/app/src/main/java/cx/ring/dependencyinjection/ServiceInjectionModule.java
index 8661d07f79596532269e9dbddc5d4b5f3fad2dd1..a590ff075d04cd7aecc5a1b510a3977c685b487a 100755
--- a/ring-android/app/src/main/java/cx/ring/dependencyinjection/ServiceInjectionModule.java
+++ b/ring-android/app/src/main/java/cx/ring/dependencyinjection/ServiceInjectionModule.java
@@ -32,6 +32,7 @@ import cx.ring.services.CallService;
 import cx.ring.services.ConferenceService;
 import cx.ring.services.ContactService;
 import cx.ring.services.ContactServiceImpl;
+import cx.ring.facades.ConversationFacade;
 import cx.ring.services.DaemonService;
 import cx.ring.services.DeviceRuntimeService;
 import cx.ring.services.DeviceRuntimeServiceImpl;
@@ -148,6 +149,19 @@ public class ServiceInjectionModule {
         return contactService;
     }
 
+    @Provides
+    @Singleton
+    ConversationFacade provideConversationtFacade(
+            AccountService accountService,
+            ContactService contactService,
+            ConferenceService conferenceService,
+            HistoryService historyService
+    ) {
+        ConversationFacade conversationFacade = new ConversationFacade(historyService);
+        mRingApplication.getRingInjectionComponent().inject(conversationFacade);
+        return conversationFacade;
+    }
+
     @Provides
     @Named("DaemonExecutor")
     @Singleton
diff --git a/ring-android/app/src/main/java/cx/ring/fragments/CallFragment.java b/ring-android/app/src/main/java/cx/ring/fragments/CallFragment.java
index 0257397a249fa10e2bc689bcf1067f7fe67a048a..9bb3bdfb9b1e26aaa7a85276373406ab6670e14a 100644
--- a/ring-android/app/src/main/java/cx/ring/fragments/CallFragment.java
+++ b/ring-android/app/src/main/java/cx/ring/fragments/CallFragment.java
@@ -80,6 +80,7 @@ import cx.ring.adapters.ContactDetailsTask;
 import cx.ring.application.RingApplication;
 import cx.ring.client.ConversationActivity;
 import cx.ring.client.HomeActivity;
+import cx.ring.facades.ConversationFacade;
 import cx.ring.interfaces.CallInterface;
 import cx.ring.model.CallContact;
 import cx.ring.model.Conference;
@@ -117,6 +118,9 @@ public class CallFragment extends Fragment implements CallInterface, ContactDeta
     @Inject
     AccountService mAccountService;
 
+    @Inject
+    ConversationFacade mConversationFacade;
+
     @Inject
     NotificationService mNotificationService;
 
@@ -590,7 +594,7 @@ public class CallFragment extends Fragment implements CallInterface, ContactDeta
         if (service == null)
             return;
 
-        Conference c = service.getConference(getConference().getId());
+        Conference c = mConversationFacade.getConference(getConference().getId());
         mCallbacks.updateDisplayedConference(c);
         if (c == null || c.getParticipants().isEmpty()) {
             mCallbacks.terminateCall();
diff --git a/ring-android/app/src/main/java/cx/ring/fragments/ConversationFragment.java b/ring-android/app/src/main/java/cx/ring/fragments/ConversationFragment.java
index f8850916033dd37deb928a04eb93573ab508635c..d5c9b50575262526ba194342628c0f223f98ef3f 100644
--- a/ring-android/app/src/main/java/cx/ring/fragments/ConversationFragment.java
+++ b/ring-android/app/src/main/java/cx/ring/fragments/ConversationFragment.java
@@ -46,6 +46,7 @@ import cx.ring.application.RingApplication;
 import cx.ring.client.CallActivity;
 import cx.ring.client.ConversationActivity;
 import cx.ring.client.HomeActivity;
+import cx.ring.facades.ConversationFacade;
 import cx.ring.model.Account;
 import cx.ring.model.CallContact;
 import cx.ring.model.Conference;
@@ -56,18 +57,18 @@ import cx.ring.model.Uri;
 import cx.ring.service.LocalService;
 import cx.ring.services.AccountService;
 import cx.ring.services.CallService;
+import cx.ring.services.ContactService;
 import cx.ring.utils.ActionHelper;
 import cx.ring.utils.ClipboardHelper;
 import cx.ring.utils.ContentUriHandler;
 import cx.ring.utils.Observable;
 import cx.ring.utils.Observer;
-import cx.ring.services.ContactService;
 
 public class ConversationFragment extends Fragment implements
         Conversation.ConversationActionCallback,
         ClipboardHelper.ClipboardHelperCallback,
         ContactDetailsTask.DetailsLoadedCallback,
-        Observer<ServiceEvent>{
+        Observer<ServiceEvent> {
 
     @Inject
     ContactService mContactService;
@@ -78,6 +79,9 @@ public class ConversationFragment extends Fragment implements
     @Inject
     AccountService mAccountService;
 
+    @Inject
+    ConversationFacade mConversationFacade;
+
     @BindView(R.id.msg_input_txt)
     EditText mMsgEditTxt;
 
@@ -110,7 +114,6 @@ public class ConversationFragment extends Fragment implements
     private ConversationAdapter mAdapter = null;
     private NumberAdapter mNumberAdapter = null;
 
-
     public static Boolean isTabletMode(Context context) {
         return context.getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE
                 && context.getResources().getConfiguration().screenWidthDp >= MIN_SIZE_TABLET;
@@ -125,7 +128,7 @@ public class ConversationFragment extends Fragment implements
         Uri number = new Uri(bundle.getString("number"));
 
         Log.d(TAG, "getConversation " + conversationId + " " + number);
-        Conversation conversation = service.getConversation(conversationId);
+        Conversation conversation = mConversationFacade.getConversationById(conversationId);
         if (conversation == null) {
             long contactId = CallContact.contactIdFromId(conversationId);
             Log.d(TAG, "no conversation found, contact_id " + contactId);
@@ -150,7 +153,7 @@ public class ConversationFragment extends Fragment implements
                     }
                 }
             }
-            conversation = service.startConversation(contact);
+            conversation = mConversationFacade.startConversation(contact);
         }
 
         Log.d(TAG, "returning " + conversation.getContact().getDisplayName() + " " + number);
@@ -307,7 +310,7 @@ public class ConversationFragment extends Fragment implements
 
         if (mVisible && mConversation != null && !mConversation.isVisible()) {
             mConversation.setVisible(true);
-            service.readConversation(mConversation);
+            mConversationFacade.readConversation(mConversation);
         }
 
         if (mDeleteConversation) {
@@ -351,8 +354,8 @@ public class ConversationFragment extends Fragment implements
         super.onPause();
         Log.d(TAG, "onPause");
         mVisible = false;
-        if (mConversation != null && mCallbacks.getService() != null) {
-            mCallbacks.getService().readConversation(mConversation);
+        if (mConversation != null ) {
+            mConversationFacade.readConversation(mConversation);
             mConversation.setVisible(false);
         }
 
@@ -367,9 +370,7 @@ public class ConversationFragment extends Fragment implements
         mVisible = true;
         if (mConversation != null) {
             mConversation.setVisible(true);
-            if (mCallbacks.getService() != null) {
-                mCallbacks.getService().readConversation(mConversation);
-            }
+            mConversationFacade.readConversation(mConversation);
         }
 
         IntentFilter filter = new IntentFilter(LocalService.ACTION_CONF_UPDATE);
@@ -508,9 +509,9 @@ public class ConversationFragment extends Fragment implements
             if (guess == null || guess.first == null) {
                 return;
             }
-            mCallbacks.getService().sendTextMessage(guess.first.getAccountID(), guess.second, txt);
+            mConversationFacade.sendTextMessage(guess.first.getAccountID(), guess.second, txt);
         } else {
-            mCallbacks.getService().sendTextMessage(conference, txt);
+            mConversationFacade.sendTextMessage(conference, txt);
         }
     }
 
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 d3b06a93bf0c7ce86d9adb7b22ef08e4871f16b6..0bed049f0feb144dc3ede62610bac0880c77bc60 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
@@ -74,6 +74,7 @@ import cx.ring.application.RingApplication;
 import cx.ring.client.ConversationActivity;
 import cx.ring.client.HomeActivity;
 import cx.ring.client.QRCodeScannerActivity;
+import cx.ring.facades.ConversationFacade;
 import cx.ring.model.Account;
 import cx.ring.model.CallContact;
 import cx.ring.model.Conference;
@@ -83,7 +84,6 @@ import cx.ring.model.ServiceEvent;
 import cx.ring.model.Uri;
 import cx.ring.service.LocalService;
 import cx.ring.services.AccountService;
-import cx.ring.services.CallService;
 import cx.ring.services.ContactService;
 import cx.ring.utils.ActionHelper;
 import cx.ring.utils.BlockchainInputHandler;
@@ -148,6 +148,9 @@ public class SmartListFragment extends Fragment implements SearchView.OnQueryTex
     @Inject
     ContactService mContactService;
 
+    @Inject
+    ConversationFacade mConversationFacade;
+
     final BroadcastReceiver receiver = new BroadcastReceiver() {
         @Override
         public void onReceive(Context context, Intent intent) {
@@ -174,8 +177,7 @@ public class SmartListFragment extends Fragment implements SearchView.OnQueryTex
                 return;
             }
 
-            LocalService service = mCallbacks.getService();
-            service.updateConversationContactWithRingId(name, address);
+            mConversationFacade.updateConversationContactWithRingId(name, address);
             RingApplication.uiHandler.post(new Runnable() {
                 @Override
                 public void run() {
@@ -224,7 +226,7 @@ public class SmartListFragment extends Fragment implements SearchView.OnQueryTex
         if (mSmartListAdapter == null) {
             bindService(getActivity(), service);
         } else {
-            mSmartListAdapter.updateDataset(service.getConversations(), null);
+            mSmartListAdapter.updateDataset(mConversationFacade.getConversationsList(), null);
         }
 
         if (service.isConnected()) {
@@ -245,7 +247,7 @@ public class SmartListFragment extends Fragment implements SearchView.OnQueryTex
         if (service == null || !service.isConnected() || !service.areConversationsLoaded()) {
             return;
         }
-        List<Conversation> conversations = service.getConversations();
+        List<Conversation> conversations = mConversationFacade.getConversationsList();
         for (Conversation conversation : conversations) {
             CallContact contact = conversation.getContact();
             if (contact == null) {
@@ -309,6 +311,7 @@ public class SmartListFragment extends Fragment implements SearchView.OnQueryTex
 
         mAccountService.addObserver(this);
         mAccountService.addObserver(mRinguifyObserver);
+        mConversationFacade.addObserver(this);
         Log.d(TAG, "onResume");
         ((HomeActivity) getActivity()).setToolbarState(false, R.string.app_name);
         if (mSmartListAdapter != null) {
@@ -455,7 +458,7 @@ public class SmartListFragment extends Fragment implements SearchView.OnQueryTex
                     Log.d(TAG, "onQueryTextChange: null service");
                 } else {
                     mSmartListAdapter.updateDataset(
-                            mCallbacks.getService().getConversations(),
+                            mConversationFacade.getConversationsList(),
                             query
                     );
                 }
@@ -508,6 +511,7 @@ public class SmartListFragment extends Fragment implements SearchView.OnQueryTex
         getActivity().unregisterReceiver(receiver);
         mAccountService.removeObserver(this);
         mAccountService.removeObserver(mRinguifyObserver);
+        mConversationFacade.removeObserver(this);
     }
 
     @OnClick(R.id.newcontact_element)
@@ -538,7 +542,7 @@ public class SmartListFragment extends Fragment implements SearchView.OnQueryTex
                 service.get40dpContactCache(),
                 service.getThreadPool());
 
-        mSmartListAdapter.updateDataset(service.getConversations(), null);
+        mSmartListAdapter.updateDataset(mConversationFacade.getConversationsList(), null);
         mSmartListAdapter.setCallback(this);
         if (mList != null) {
             mList.setAdapter(mSmartListAdapter);
@@ -863,54 +867,59 @@ public class SmartListFragment extends Fragment implements SearchView.OnQueryTex
         mNewContact.setVisibility(View.VISIBLE);
     }
 
+    private void handleRegisterNameFound(final String name, final String address, final int state) {
+        RingApplication.uiHandler.post(new Runnable() {
+            @Override
+            public void run() {
+                switch (state) {
+                    case 0:
+                        // on found
+                        if (!TextUtils.isEmpty(mLastBlockchainQuery) && mLastBlockchainQuery.equals(name)) {
+                            displayNewContactRowWithName(name, address);
+                            mLastBlockchainQuery = null;
+                        } else {
+                            mNewContact.setVisibility(View.GONE);
+                        }
+                        break;
+                    case 1:
+                        // invalid name
+                        Uri uriName = new Uri(name);
+                        if (uriName.isRingId()) {
+                            displayNewContactRowWithName(name, null);
+                        } else {
+                            mNewContact.setVisibility(View.GONE);
+                        }
+                        break;
+                    default:
+                        // on error
+                        Uri uriAddress = new Uri(address);
+                        if (uriAddress.isRingId()) {
+                            displayNewContactRowWithName(name, address);
+                        } else {
+                            mNewContact.setVisibility(View.GONE);
+                        }
+                        break;
+                }
+            }
+        });
+    }
+
     @Override
     public void update(Observable observable, final ServiceEvent event) {
         if (event == null) {
             return;
         }
 
-        if (event.getEventType() == ServiceEvent.EventType.REGISTERED_NAME_FOUND) {
-            if (!isSearching) {
-                return;
-            }
-            RingApplication.uiHandler.post(new Runnable() {
-                @Override
-                public void run() {
-                    String name = event.getEventInput(ServiceEvent.EventInput.NAME, String.class);
-                    String address = event.getEventInput(ServiceEvent.EventInput.ADDRESS, String.class);
-
-                    int state = event.getEventInput(ServiceEvent.EventInput.STATE, Integer.class);
-                    switch (state) {
-                        case 0:
-                            // on found
-                            if (!TextUtils.isEmpty(mLastBlockchainQuery) && mLastBlockchainQuery.equals(name)) {
-                                displayNewContactRowWithName(name, address);
-                                mLastBlockchainQuery = null;
-                            } else {
-                                mNewContact.setVisibility(View.GONE);
-                            }
-                            break;
-                        case 1:
-                            // invalid name
-                            Uri uriName = new Uri(name);
-                            if (uriName.isRingId()) {
-                                displayNewContactRowWithName(name, null);
-                            } else {
-                                mNewContact.setVisibility(View.GONE);
-                            }
-                            break;
-                        default:
-                            // on error
-                            Uri uriAddress = new Uri(address);
-                            if (uriAddress.isRingId()) {
-                                displayNewContactRowWithName(name, address);
-                            } else {
-                                mNewContact.setVisibility(View.GONE);
-                            }
-                            break;
-                    }
+        switch (event.getEventType()) {
+            case REGISTERED_NAME_FOUND:
+                String name = event.getEventInput(ServiceEvent.EventInput.NAME, String.class);
+                if (TextUtils.isEmpty(mLastBlockchainQuery) || !mLastBlockchainQuery.equals(name)) {
+                    return;
                 }
-            });
+                String address = event.getEventInput(ServiceEvent.EventInput.ADDRESS, String.class);
+                int state = event.getEventInput(ServiceEvent.EventInput.STATE, Integer.class);
+                handleRegisterNameFound(name, address, state);
+                break;
         }
     }
 }
diff --git a/ring-android/app/src/main/java/cx/ring/service/LocalService.java b/ring-android/app/src/main/java/cx/ring/service/LocalService.java
index bb523ce0a54b36ac16abebcd604a7e7e85c258be..0ed8665497dc329ab8dbd068edcbd87ea0cc2671 100644
--- a/ring-android/app/src/main/java/cx/ring/service/LocalService.java
+++ b/ring-android/app/src/main/java/cx/ring/service/LocalService.java
@@ -22,7 +22,6 @@ package cx.ring.service;
 import android.app.Service;
 import android.content.BroadcastReceiver;
 import android.content.ComponentName;
-import android.content.ContentResolver;
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
@@ -33,26 +32,17 @@ import android.graphics.Bitmap;
 import android.media.AudioManager;
 import android.net.ConnectivityManager;
 import android.net.NetworkInfo;
-import android.os.AsyncTask;
 import android.os.Binder;
 import android.os.IBinder;
 import android.os.RemoteException;
 import android.preference.PreferenceManager;
 import android.provider.ContactsContract;
-import android.text.TextUtils;
 import android.util.Log;
 import android.util.LongSparseArray;
 import android.util.LruCache;
-import android.util.Pair;
 
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Comparator;
 import java.util.HashMap;
-import java.util.List;
 import java.util.Map;
-import java.util.NavigableMap;
-import java.util.TreeMap;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
 
@@ -60,14 +50,13 @@ import javax.inject.Inject;
 
 import cx.ring.BuildConfig;
 import cx.ring.application.RingApplication;
+import cx.ring.facades.ConversationFacade;
 import cx.ring.model.Account;
 import cx.ring.model.CallContact;
 import cx.ring.model.Conference;
 import cx.ring.model.ConfigKey;
 import cx.ring.model.Conversation;
 import cx.ring.model.HistoryCall;
-import cx.ring.model.HistoryEntry;
-import cx.ring.model.HistoryText;
 import cx.ring.model.SecureSipCall;
 import cx.ring.model.ServiceEvent;
 import cx.ring.model.Settings;
@@ -86,8 +75,6 @@ import cx.ring.utils.ContentUriHandler;
 import cx.ring.utils.MediaManager;
 import cx.ring.utils.Observable;
 import cx.ring.utils.Observer;
-import cx.ring.utils.Tuple;
-
 
 public class LocalService extends Service implements Observer<ServiceEvent> {
     static final String TAG = LocalService.class.getSimpleName();
@@ -127,6 +114,9 @@ public class LocalService extends Service implements Observer<ServiceEvent> {
     @Inject
     HardwareService mHardwareService;
 
+    @Inject
+    ConversationFacade mConversationFacade;
+
     private IDRingService mService = null;
     private boolean dringStarted = false;
 
@@ -135,9 +125,6 @@ public class LocalService extends Service implements Observer<ServiceEvent> {
     // Binder given to clients
     private final IBinder mBinder = new LocalBinder();
 
-    private Map<String, Conversation> conversations = new HashMap<>();
-    private LongSparseArray<TextMessage> messages = new LongSparseArray<>();
-
     private LruCache<Long, Bitmap> mMemoryCache = null;
     private final ExecutorService mPool = Executors.newCachedThreadPool();
 
@@ -146,7 +133,6 @@ public class LocalService extends Service implements Observer<ServiceEvent> {
     private boolean isWifiConn = false;
     private boolean isMobileConn = false;
 
-    private boolean canUseContacts = true;
     private boolean canUseMobile = false;
 
     private boolean mAreConversationsLoaded = false;
@@ -163,132 +149,10 @@ public class LocalService extends Service implements Observer<ServiceEvent> {
         return isWifiConn || (canUseMobile && isMobileConn);
     }
 
-    public boolean isWifiConnected() {
-        return isWifiConn;
-    }
-
     public boolean isMobileNetworkConnectedButNotGranted() {
         return (!canUseMobile && isMobileConn);
     }
 
-    public Conference placeCall(SipCall call) {
-        Conference conf = null;
-        CallContact contact = call.getContact();
-        if (contact == null) {
-            contact = mContactService.findContactByNumber(call.getNumberUri().getRawUriString());
-        }
-        Conversation conv = startConversation(contact);
-        try {
-            mHardwareService.setPreviewSettings(mDeviceRuntimeService.retrieveAvailablePreviewSettings());
-            Uri number = call.getNumberUri();
-            if (number == null || number.isEmpty()) {
-                number = contact.getPhones().get(0).getNumber();
-            }
-            String callId = mService.placeCall(call.getAccount(), number.getUriString(), !call.isVideoMuted());
-            if (callId == null || callId.isEmpty()) {
-                return null;
-            }
-            call.setCallID(callId);
-            Account account = mAccountService.getAccount(call.getAccount());
-            if (account.isRing()
-                    || account.getDetailBoolean(ConfigKey.SRTP_ENABLE)
-                    || account.getDetailBoolean(ConfigKey.TLS_ENABLE)) {
-                Log.i(TAG, "placeCall() call is secure");
-                SecureSipCall secureCall = new SecureSipCall(call, account.getDetail(ConfigKey.SRTP_KEY_EXCHANGE));
-                conf = new Conference(secureCall);
-            } else {
-                conf = new Conference(call);
-            }
-            conf.getParticipants().get(0).setContact(contact);
-            conv.addConference(conf);
-        } catch (RemoteException e) {
-            Log.e(TAG, "placeCall", e);
-        }
-        return conf;
-    }
-
-    public void sendTextMessage(String account, Uri to, String txt) {
-        try {
-            long id = mService.sendAccountTextMessage(account, to.getRawUriString(), txt);
-            Log.i(TAG, "sendAccountTextMessage " + txt + " got id " + id);
-            TextMessage message = new TextMessage(false, txt, to, null, account);
-            message.setID(id);
-            message.read();
-            mHistoryService.insertNewTextMessage(message);
-            messages.put(id, message);
-            textMessageSent(message);
-        } catch (RemoteException e) {
-            Log.e(TAG, "sendTextMessage", e);
-        }
-    }
-
-    public void sendTextMessage(Conference conf, String txt) {
-        try {
-            mService.sendTextMessage(conf.getId(), txt);
-            SipCall call = conf.getParticipants().get(0);
-            TextMessage message = new TextMessage(false, txt, call.getNumberUri(), conf.getId(), call.getAccount());
-            message.read();
-            mHistoryService.insertNewTextMessage(message);
-            textMessageSent(message);
-        } catch (RemoteException e) {
-            Log.e(TAG, "sendTextMessage", e);
-        }
-    }
-
-    private void readTextMessage(TextMessage message) {
-        message.read();
-        HistoryText ht = new HistoryText(message);
-        mHistoryService.updateTextMessage(ht);
-    }
-
-    public void readConversation(Conversation conv) {
-        for (HistoryEntry h : conv.getRawHistory().values()) {
-            NavigableMap<Long, TextMessage> messages = h.getTextMessages();
-            for (TextMessage msg : messages.descendingMap().values()) {
-                if (msg.isRead()) {
-                    break;
-                }
-                readTextMessage(msg);
-            }
-        }
-        mNotificationService.cancelTextNotification(conv.getContact());
-        updateTextNotifications();
-    }
-
-    private Conversation conversationFromMessage(TextMessage txt) {
-        Conversation conv;
-        String call = txt.getCallId();
-        if (call != null && !call.isEmpty()) {
-            conv = getConversationByCallId(call);
-        } else {
-            conv = startConversation(mContactService.findContactByNumber(txt.getNumberUri().getRawUriString()));
-            txt.setContact(conv.getContact());
-        }
-        return conv;
-    }
-
-    private void textMessageSent(TextMessage txt) {
-        Log.d(TAG, "Sent text messsage " + txt.getAccount() + " " + txt.getCallId() + " " + txt.getNumberUri() + " " + txt.getMessage());
-        Conversation conv = conversationFromMessage(txt);
-        conv.addTextMessage(txt);
-        if (conv.isVisible()) {
-            txt.read();
-        } else {
-            updateTextNotifications();
-        }
-        sendBroadcast(new Intent(ACTION_CONF_UPDATE));
-    }
-
-    public void refreshConversations() {
-        Log.d(TAG, "refreshConversations()");
-        new ConversationLoader(getApplicationContext().getContentResolver()) {
-            @Override
-            protected void onPostExecute(Map<String, Conversation> res) {
-                updated(res);
-            }
-        }.execute();
-    }
-
     public void reloadAccounts() {
         if (mService != null) {
             //initAccountLoader();
@@ -345,12 +209,12 @@ public class LocalService extends Service implements Observer<ServiceEvent> {
         mSettingsService.addObserver(this);
         mAccountService.addObserver(this);
         mContactService.addObserver(this);
+        mConversationFacade.addObserver(this);
 
         // Clear any notifications from a previous app instance
         mNotificationService.cancelAll();
 
         Settings settings = mSettingsService.loadSettings();
-        canUseContacts = settings.isAllowSystemContacts();
         canUseMobile = settings.isAllowMobileData();
 
         startDRingService();
@@ -384,6 +248,7 @@ public class LocalService extends Service implements Observer<ServiceEvent> {
         mSettingsService.removeObserver(this);
         mAccountService.removeObserver(this);
         mContactService.removeObserver(this);
+        mConversationFacade.removeObserver(this);
         stopListener();
         mMemoryCache.evictAll();
         mPool.shutdown();
@@ -394,7 +259,7 @@ public class LocalService extends Service implements Observer<ServiceEvent> {
         public void onServiceConnected(ComponentName className, IBinder service) {
             Log.w(TAG, "onServiceConnected " + className.getClassName());
             mService = IDRingService.Stub.asInterface(service);
-            refreshConversations();
+            mConversationFacade.refreshConversations();
         }
 
         @Override
@@ -443,275 +308,8 @@ public class LocalService extends Service implements Observer<ServiceEvent> {
         return mService;
     }
 
-    public ArrayList<Conversation> getConversations() {
-        ArrayList<Conversation> convs = new ArrayList<>(conversations.values());
-        Collections.sort(convs, new Comparator<Conversation>() {
-            @Override
-            public int compare(Conversation lhs, Conversation rhs) {
-                return (int) ((rhs.getLastInteraction().getTime() - lhs.getLastInteraction().getTime()) / 1000l);
-            }
-        });
-        return convs;
-    }
-
-    public Conversation getConversation(String id) {
-        return conversations.get(id);
-    }
-
-    public Conference getConference(String id) {
-        for (Conversation conv : conversations.values()) {
-            Conference conf = conv.getConference(id);
-            if (conf != null) {
-                return conf;
-            }
-        }
-        return null;
-    }
-
-    public Pair<Conference, SipCall> getCall(String id) {
-        for (Conversation conv : conversations.values()) {
-            ArrayList<Conference> confs = conv.getCurrentCalls();
-            for (Conference c : confs) {
-                SipCall call = c.getCallById(id);
-                if (call != null) {
-                    return new Pair<>(c, call);
-                }
-            }
-        }
-        return new Pair<>(null, null);
-    }
-
-    public Conversation getByContact(CallContact contact) {
-        ArrayList<String> keys = contact.getIds();
-        for (String key : keys) {
-            Conversation conversation = conversations.get(key);
-            if (conversation != null) {
-                return conversation;
-            }
-        }
-        Log.w(TAG, "getByContact failed");
-        return null;
-    }
-
-    public Conversation getConversationByCallId(String callId) {
-        for (Conversation conversation : conversations.values()) {
-            Conference conf = conversation.getConference(callId);
-            if (conf != null) {
-                return conversation;
-            }
-        }
-        return null;
-    }
-
-    public Conversation startConversation(CallContact contact) {
-        if (contact.isUnknown()) {
-            contact = mContactService.findContactByNumber(contact.getPhones().get(0).getNumber().getRawUriString());
-        }
-        Conversation conversation = getByContact(contact);
-        if (conversation == null) {
-            conversation = new Conversation(contact);
-            conversations.put(contact.getIds().get(0), conversation);
-        }
-        return conversation;
-    }
-
-    public void updateConversationContactWithRingId(String oldId, String ringId) {
-
-        if (TextUtils.isEmpty(oldId)) {
-            return;
-        }
-
-        Uri uri = new Uri(oldId);
-        if (uri.isRingId()) {
-            return;
-        }
-
-        Conversation conversation = conversations.get(oldId);
-        if (conversation == null) {
-            return;
-        }
-
-        CallContact contact = conversation.getContact();
-
-        if (contact == null) {
-            return;
-        }
-
-        Uri ringIdUri = new Uri(ringId);
-        contact.getPhones().clear();
-        contact.getPhones().add(new cx.ring.model.Phone(ringIdUri, 0));
-        contact.resetDisplayName();
-
-        conversations.remove(oldId);
-        conversations.put(contact.getIds().get(0), conversation);
-
-        for (Map.Entry<String, HistoryEntry> entry : conversation.getHistory().entrySet()) {
-            HistoryEntry historyEntry = entry.getValue();
-            historyEntry.setContact(contact);
-            NavigableMap<Long, TextMessage> messages = historyEntry.getTextMessages();
-            for (TextMessage textMessage : messages.values()) {
-                textMessage.setNumber(ringIdUri);
-                textMessage.setContact(contact);
-                mHistoryService.updateTextMessage(new HistoryText(textMessage));
-            }
-        }
-
-        return;
-    }
-
-    public Conversation findConversationByNumber(Uri number) {
-        if (number == null || number.isEmpty()) {
-            return null;
-        }
-        for (Conversation conversation : conversations.values()) {
-            if (conversation.getContact().hasNumber(number)) {
-                return conversation;
-            }
-        }
-        return startConversation(mContactService.findContactByNumber(number.getRawUriString()));
-    }
-
-    public void clearHistory() {
-        mHistoryService.clearHistory();
-        refreshConversations();
-    }
-
-    private class ConversationLoader extends AsyncTask<Void, Void, Map<String, Conversation>> {
-        private final ContentResolver cr;
-
-        public ConversationLoader(ContentResolver c) {
-            cr = c;
-        }
-
-        Tuple<HistoryEntry, HistoryCall> findHistoryByCallId(final Map<String, Conversation> confs, String id) {
-            for (Conversation c : confs.values()) {
-                Tuple<HistoryEntry, HistoryCall> h = c.findHistoryByCallId(id);
-                if (h != null) {
-                    return h;
-                }
-            }
-            return null;
-        }
-
-        @Override
-        protected Map<String, Conversation> doInBackground(Void... params) {
-            final Map<String, Conversation> ret = new HashMap<>();
-
-            if (mService == null) {
-                return ret;
-            }
-
-            try {
-                final List<HistoryCall> history = mHistoryService.getAll();
-                final List<HistoryText> historyTexts = mHistoryService.getAllTextMessages();
-
-                final Map<String, ArrayList<String>> confs = mService.getConferenceList();
-
-                for (HistoryCall call : history) {
-                    CallContact contact = mContactService.findContact(call.getContactID(), call.getContactKey(), call.getNumber());
-
-                    String key = contact.getIds().get(0);
-                    if (conversations.containsKey(key)) {
-                        if (!conversations.get(key).getHistoryCalls().contains(call)) {
-                            conversations.get(key).addHistoryCall(call);
-                        }
-                    } else {
-                        Conversation conversation = new Conversation(contact);
-                        conversation.addHistoryCall(call);
-                        conversations.put(key, conversation);
-                    }
-                }
-
-                for (HistoryText htext : historyTexts) {
-                    CallContact contact = mContactService.findContact(htext.getContactID(), htext.getContactKey(), htext.getNumber());
-                    Tuple<HistoryEntry, HistoryCall> p = findHistoryByCallId(ret, htext.getCallId());
-
-                    if (contact == null && p != null) {
-                        contact = p.first.getContact();
-                    }
-                    if (contact == null) {
-                        continue;
-                    }
-
-                    TextMessage msg = new TextMessage(htext);
-                    msg.setContact(contact);
-
-                    if (p != null) {
-                        if (msg.getNumberUri() == null) {
-                            msg.setNumber(new Uri(p.second.getNumber()));
-                        }
-                        p.first.addTextMessage(msg);
-                    }
-
-                    String key = contact.getIds().get(0);
-                    if (conversations.containsKey(key)) {
-                        if (!conversations.get(key).getTextMessages().contains(msg)) {
-                            conversations.get(key).addTextMessage(msg);
-                        }
-                    } else {
-                        Conversation c = new Conversation(contact);
-                        c.addTextMessage(msg);
-                        conversations.put(key, c);
-                    }
-                }
-
-                for (Map.Entry<String, ArrayList<String>> c : confs.entrySet()) {
-                    Conference conf = new Conference(c.getKey());
-                    for (String call_id : c.getValue()) {
-                        SipCall call = getCall(call_id).second;
-                        if (call == null) {
-                            call = new SipCall(call_id, mService.getCallDetails(call_id));
-                        }
-                        Account acc = mAccountService.getAccount(call.getAccount());
-                        if (acc.isRing()
-                                || acc.getDetailBoolean(ConfigKey.SRTP_ENABLE)
-                                || acc.getDetailBoolean(ConfigKey.TLS_ENABLE)) {
-                            call = new SecureSipCall(call, acc.getDetail(ConfigKey.SRTP_KEY_EXCHANGE));
-                        }
-                        conf.addParticipant(call);
-                    }
-                    List<SipCall> calls = conf.getParticipants();
-                    if (calls.size() == 1) {
-                        SipCall call = calls.get(0);
-                        CallContact contact = mContactService.findContact(-1, null, call.getNumber());
-                        call.setContact(contact);
-
-                        Conversation conv = null;
-                        ArrayList<String> ids = contact.getIds();
-                        for (String id : ids) {
-                            conv = conversations.get(id);
-                            if (conv != null) {
-                                break;
-                            }
-                        }
-                        if (conv != null) {
-                            conv.addConference(conf);
-                        } else {
-                            conv = new Conversation(contact);
-                            conv.addConference(conf);
-                            conversations.put(ids.get(0), conv);
-                        }
-                    }
-                }
-                for (Conversation c : conversations.values()) {
-                    Log.w(TAG, "Conversation : " + c.getContact().getId() + " " + c.getContact().getDisplayName() + " " + c.getLastNumberUsed(c.getLastAccountUsed()) + " " + c.getLastInteraction().toString());
-                }
-
-                for (CallContact contact : mContactService.getContacts()) {
-                    String key = contact.getIds().get(0);
-                    if (!conversations.containsKey(key)) {
-                        conversations.put(key, new Conversation(contact));
-                    }
-                }
-            } catch (Exception e) {
-                Log.e(TAG, "ConversationLoader doInBackground", e);
-            }
-            return conversations;
-        }
-    }
-
     private void updated(Map<String, Conversation> res) {
-        for (Conversation conversation : conversations.values()) {
+        for (Conversation conversation : mConversationFacade.getConversations().values()) {
             boolean isConversationVisible = conversation.isVisible();
             String conversationKey = conversation.getContact().getIds().get(0);
             Conversation newConversation = res.get(conversationKey);
@@ -719,9 +317,8 @@ public class LocalService extends Service implements Observer<ServiceEvent> {
                 newConversation.setVisible(isConversationVisible);
             }
         }
-        conversations = res;
         updateAudioState();
-        updateTextNotifications();
+        mConversationFacade.updateTextNotifications();
         sendBroadcast(new Intent(ACTION_CONF_UPDATE));
         sendBroadcast(new Intent(ACTION_CONF_LOADED));
         mAreConversationsLoaded = true;
@@ -748,32 +345,10 @@ public class LocalService extends Service implements Observer<ServiceEvent> {
         }
     }
 
-    public void updateTextNotifications() {
-        Log.d(TAG, "updateTextNotifications()");
-
-        for (Conversation conversation : conversations.values()) {
-
-            if (conversation.isVisible()) {
-                mNotificationService.cancelTextNotification(conversation.getContact());
-                continue;
-            }
-
-            TreeMap<Long, TextMessage> texts = conversation.getUnreadTextMessages();
-            if (texts.isEmpty() || texts.lastEntry().getValue().isNotified()) {
-                continue;
-            } else {
-                mNotificationService.cancelTextNotification(conversation.getContact());
-            }
-
-            CallContact contact = conversation.getContact();
-            mNotificationService.showTextNotification(contact, conversation, texts);
-        }
-    }
-
     private void updateAudioState() {
         boolean current = false;
         Conference ringing = null;
-        for (Conversation c : conversations.values()) {
+        for (Conversation c : mConversationFacade.getConversations().values()) {
             Conference conf = c.getCurrentCall();
             if (conf != null) {
                 current = true;
@@ -816,9 +391,9 @@ public class LocalService extends Service implements Observer<ServiceEvent> {
                 }
                 case ACTION_CONV_READ: {
                     String convId = intent.getData().getLastPathSegment();
-                    Conversation conversation = getConversation(convId);
+                    Conversation conversation = mConversationFacade.getConversationById(convId);
                     if (conversation != null) {
-                        readConversation(conversation);
+                        mConversationFacade.readConversation(conversation);
                     }
 
                     sendBroadcast(new Intent(ACTION_CONF_UPDATE).setData(android.net.Uri.withAppendedPath(ContentUriHandler.CONVERSATION_CONTENT_URI, convId)));
@@ -832,7 +407,7 @@ public class LocalService extends Service implements Observer<ServiceEvent> {
                         Log.e(TAG, "ACTION_CALL_ACCEPT", e);
                     }
                     updateAudioState();
-                    Conference conf = getConference(callId);
+                    Conference conf = mConversationFacade.getConference(callId);
                     if (conf != null && !conf.isVisible()) {
                         startActivity(ActionHelper.getViewIntent(LocalService.this, conf).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
                     }
@@ -874,9 +449,9 @@ public class LocalService extends Service implements Observer<ServiceEvent> {
 
                     Conversation conversation;
                     if (call != null && !call.isEmpty()) {
-                        conversation = getConversationByCallId(call);
+                        conversation = mConversationFacade.getConversationByCallId(call);
                     } else {
-                        conversation = startConversation(mContactService.findContactByNumber(txt.getNumberUri().getRawUriString()));
+                        conversation = mConversationFacade.startConversation(mContactService.findContactByNumber(txt.getNumberUri().getRawUriString()));
                         txt.setContact(conversation.getContact());
                     }
                     if (conversation.isVisible()) {
@@ -890,28 +465,12 @@ public class LocalService extends Service implements Observer<ServiceEvent> {
                     sendBroadcast(new Intent(ACTION_CONF_UPDATE));
                     break;
                 }
-                case ConfigurationManagerCallback.MESSAGE_STATE_CHANGED: {
-                    long id = intent.getLongExtra(ConfigurationManagerCallback.MESSAGE_STATE_CHANGED_EXTRA_ID, 0);
-                    int status = intent.getIntExtra(
-                            ConfigurationManagerCallback.MESSAGE_STATE_CHANGED_EXTRA_STATUS,
-                            TextMessage.Status.UNKNOWN.toInt()
-                    );
-                    TextMessage msg = messages.get(id);
-                    if (msg != null) {
-                        Log.d(TAG, "Message status changed " + id + " " + status);
-                        msg.setStatus(status);
-                        sendBroadcast(new Intent(ACTION_CONF_UPDATE).
-                                putExtra(ACTION_CONF_UPDATE_EXTRA_MSG, id)
-                        );
-                    }
-                    break;
-                }
                 case CallManagerCallBack.INCOMING_CALL: {
                     String callId = intent.getStringExtra("call");
                     String accountId = intent.getStringExtra("account");
                     Uri number = new Uri(intent.getStringExtra("from"));
                     CallContact contact = mContactService.findContactByNumber(number.getRawUriString());
-                    Conversation conversation = startConversation(contact);
+                    Conversation conversation = mConversationFacade.startConversation(contact);
 
                     SipCall call = new SipCall(callId, accountId, number, SipCall.Direction.INCOMING);
                     call.setContact(contact);
@@ -953,7 +512,7 @@ public class LocalService extends Service implements Observer<ServiceEvent> {
                     Conversation conversation = null;
                     Conference found = null;
 
-                    for (Conversation conv : conversations.values()) {
+                    for (Conversation conv : mConversationFacade.getConversations().values()) {
                         Conference tconf = conv.getConference(callId);
                         if (tconf != null) {
                             conversation = conv;
@@ -1028,7 +587,7 @@ public class LocalService extends Service implements Observer<ServiceEvent> {
                     // no refresh here
                     break;
                 default:
-                    refreshConversations();
+                    mConversationFacade.refreshConversations();
             }
         }
     };
@@ -1093,18 +652,27 @@ public class LocalService extends Service implements Observer<ServiceEvent> {
 
     public void deleteConversation(Conversation conversation) {
         mHistoryService.clearHistoryForConversation(conversation);
-        refreshConversations();
     }
 
     @Override
     public void update(Observable observable, ServiceEvent arg) {
 
         if (observable instanceof HistoryService) {
-            refreshConversations();
+
+            if(arg != null) {
+                switch (arg.getEventType()) {
+                    case HISTORY_LOADED:
+                        break;
+                    default:
+                        mConversationFacade.refreshConversations();
+                        break;
+                }
+            } else {
+                mConversationFacade.refreshConversations();
+            }
         }
 
         if (observable instanceof SettingsService) {
-            canUseContacts = mSettingsService.loadSettings().isAllowSystemContacts();
             canUseMobile = mSettingsService.loadSettings().isAllowMobileData();
             refreshContacts();
             updateConnectivityState();
@@ -1127,7 +695,16 @@ public class LocalService extends Service implements Observer<ServiceEvent> {
         if (observable instanceof ContactService && arg != null) {
             switch (arg.getEventType()) {
                 case CONTACTS_CHANGED:
-                    refreshConversations();
+                    mConversationFacade.refreshConversations();
+                    break;
+            }
+        }
+
+        if (observable instanceof ConversationFacade && arg != null) {
+            switch (arg.getEventType()) {
+                case CONVERSATIONS_CHANGED:
+                    Map<String, Conversation> conversations = mConversationFacade.getConversations();
+                    updated(conversations);
                     break;
             }
         }
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
new file mode 100644
index 0000000000000000000000000000000000000000..4e31737e8c3fc369857ceeaf4efd21e2c609aa9d
--- /dev/null
+++ b/ring-android/libringclient/src/main/java/cx/ring/facades/ConversationFacade.java
@@ -0,0 +1,496 @@
+/*
+ *  Copyright (C) 2016 Savoir-faire Linux Inc.
+ *
+ *  Author: Thibault Wittemberg <thibault.wittemberg@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.facades;
+
+import java.sql.SQLException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.NavigableMap;
+import java.util.TreeMap;
+
+import javax.inject.Inject;
+
+import cx.ring.model.Account;
+import cx.ring.model.CallContact;
+import cx.ring.model.Conference;
+import cx.ring.model.ConfigKey;
+import cx.ring.model.Conversation;
+import cx.ring.model.HistoryCall;
+import cx.ring.model.HistoryEntry;
+import cx.ring.model.HistoryText;
+import cx.ring.model.SecureSipCall;
+import cx.ring.model.ServiceEvent;
+import cx.ring.model.SipCall;
+import cx.ring.model.TextMessage;
+import cx.ring.model.Uri;
+import cx.ring.services.AccountService;
+import cx.ring.services.CallService;
+import cx.ring.services.ConferenceService;
+import cx.ring.services.ContactService;
+import cx.ring.services.DeviceRuntimeService;
+import cx.ring.services.HardwareService;
+import cx.ring.services.HistoryService;
+import cx.ring.services.NotificationService;
+import cx.ring.utils.Log;
+import cx.ring.utils.Observable;
+import cx.ring.utils.Observer;
+import cx.ring.utils.Tuple;
+
+/**
+ * This facade handles the conversations
+ * - Load from the history
+ * - Keep a local cache of these conversations
+ * <p>
+ * Events are broadcasted:
+ * - CONVERSATIONS_CHANGED
+ */
+public class ConversationFacade extends Observable implements Observer<ServiceEvent> {
+
+    private final static String TAG = ConversationFacade.class.getName();
+
+    @Inject
+    AccountService mAccountService;
+
+    @Inject
+    ContactService mContactService;
+
+    @Inject
+    ConferenceService mConferenceService;
+
+    private HistoryService mHistoryService;
+
+    @Inject
+    CallService mCallService;
+
+    @Inject
+    HardwareService mHardwareService;
+
+    @Inject
+    NotificationService mNotificationService;
+
+    @Inject
+    DeviceRuntimeService mDeviceRuntimeService;
+
+    private Map<String, Conversation> mConversationMap;
+
+    public ConversationFacade(HistoryService historyService) {
+        mConversationMap = new HashMap<>();
+        mHistoryService = historyService;
+        mHistoryService.addObserver(this);
+    }
+
+    /**
+     * Loads conversations from history calls and texts (also sends CONVERSATIONS_CHANGED event)
+     */
+    public void loadConversationsFromHistory() {
+        try {
+            mHistoryService.getCallAndTextAsync();
+        } catch (SQLException e) {
+            Log.e(TAG, "unable to retrieve history calls and texts", e);
+            return;
+        }
+
+    }
+
+    private Tuple<HistoryEntry, HistoryCall> findHistoryByCallId(final Map<String, Conversation> conversations, String id) {
+        for (Conversation conversation : conversations.values()) {
+            Tuple<HistoryEntry, HistoryCall> historyCall = conversation.findHistoryByCallId(id);
+            if (historyCall != null) {
+                return historyCall;
+            }
+        }
+        return null;
+    }
+
+    private Tuple<Conference, SipCall> getCall(String id) {
+        for (Conversation conv : mConversationMap.values()) {
+            ArrayList<Conference> confs = conv.getCurrentCalls();
+            for (Conference c : confs) {
+                SipCall call = c.getCallById(id);
+                if (call != null) {
+                    return new Tuple<>(c, call);
+                }
+            }
+        }
+        return new Tuple<>(null, null);
+    }
+
+    /**
+     * @return the local cache of conversations
+     */
+    public Map<String, Conversation> getConversations() {
+        return mConversationMap;
+    }
+
+    /**
+     * @param contact
+     * @return
+     */
+    public Conversation getConversationByContact(CallContact contact) {
+        ArrayList<String> keys = contact.getIds();
+        for (String key : keys) {
+            Conversation conversation = mConversationMap.get(key);
+            if (conversation != null) {
+                return conversation;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * @param callId
+     * @return
+     */
+    public Conversation getConversationByCallId(String callId) {
+        for (Conversation conversation : mConversationMap.values()) {
+            Conference conf = conversation.getConference(callId);
+            if (conf != null) {
+                return conversation;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * @param contact
+     * @return the started new conversation
+     */
+    public Conversation startConversation(CallContact contact) {
+        if (contact.isUnknown()) {
+            contact = mContactService.findContactByNumber(contact.getPhones().get(0).getNumber().getRawUriString());
+        }
+        Conversation conversation = getConversationByContact(contact);
+        if (conversation == null) {
+            conversation = new Conversation(contact);
+            mConversationMap.put(contact.getIds().get(0), conversation);
+        }
+
+        setChanged();
+        ServiceEvent event = new ServiceEvent(ServiceEvent.EventType.CONVERSATIONS_CHANGED);
+        notifyObservers(event);
+
+        return conversation;
+    }
+
+    /**
+     * @return the conversation local cache in a List
+     */
+    public ArrayList<Conversation> getConversationsList() {
+        ArrayList<Conversation> convs = new ArrayList<>(mConversationMap.values());
+        Collections.sort(convs, new Comparator<Conversation>() {
+            @Override
+            public int compare(Conversation lhs, Conversation rhs) {
+                return (int) ((rhs.getLastInteraction().getTime() - lhs.getLastInteraction().getTime()) / 1000l);
+            }
+        });
+        return convs;
+    }
+
+    /**
+     * @param id
+     * @return the conversation from the local cache
+     */
+    public Conversation getConversationById(String id) {
+        return mConversationMap.get(id);
+    }
+
+    /**
+     * (also sends CONVERSATIONS_CHANGED event)
+     *
+     * @param oldId
+     * @param ringId
+     */
+    public void updateConversationContactWithRingId(String oldId, String ringId) {
+
+        if (oldId == null || oldId.isEmpty()) {
+            return;
+        }
+
+        Uri uri = new Uri(oldId);
+        if (uri.isRingId()) {
+            return;
+        }
+
+        Conversation conversation = mConversationMap.get(oldId);
+        if (conversation == null) {
+            return;
+        }
+
+        CallContact contact = conversation.getContact();
+
+        if (contact == null) {
+            return;
+        }
+
+        Uri ringIdUri = new Uri(ringId);
+        contact.getPhones().clear();
+        contact.getPhones().add(new cx.ring.model.Phone(ringIdUri, 0));
+        contact.resetDisplayName();
+
+        mConversationMap.remove(oldId);
+        mConversationMap.put(contact.getIds().get(0), conversation);
+
+        for (Map.Entry<String, HistoryEntry> entry : conversation.getHistory().entrySet()) {
+            HistoryEntry historyEntry = entry.getValue();
+            historyEntry.setContact(contact);
+            NavigableMap<Long, TextMessage> messages = historyEntry.getTextMessages();
+            for (TextMessage textMessage : messages.values()) {
+                textMessage.setNumber(ringIdUri);
+                textMessage.setContact(contact);
+                mHistoryService.updateTextMessage(new HistoryText(textMessage));
+            }
+        }
+
+        setChanged();
+        ServiceEvent event = new ServiceEvent(ServiceEvent.EventType.CONVERSATIONS_CHANGED);
+        notifyObservers(event);
+    }
+
+    public Conversation findOrStartConversationByNumber(Uri number) {
+        if (number == null || number.isEmpty()) {
+            return null;
+        }
+
+        for (Conversation conversation : mConversationMap.values()) {
+            if (conversation.getContact().hasNumber(number)) {
+                return conversation;
+            }
+        }
+
+        return startConversation(mContactService.findContactByNumber(number.getRawUriString()));
+    }
+
+    public Conversation getConversationFromMessage(TextMessage txt) {
+        Conversation conv;
+        String call = txt.getCallId();
+        if (call != null && !call.isEmpty()) {
+            conv = getConversationByCallId(call);
+        } else {
+            conv = startConversation(mContactService.findContactByNumber(txt.getNumberUri().getRawUriString()));
+            txt.setContact(conv.getContact());
+        }
+        return conv;
+    }
+
+    public Conference placeCall(SipCall call) {
+        Conference conf;
+        CallContact contact = call.getContact();
+        if (contact == null) {
+            contact = mContactService.findContactByNumber(call.getNumberUri().getRawUriString());
+        }
+        Conversation conv = startConversation(contact);
+        mHardwareService.setPreviewSettings(mDeviceRuntimeService.retrieveAvailablePreviewSettings());
+        Uri number = call.getNumberUri();
+        if (number == null || number.isEmpty()) {
+            number = contact.getPhones().get(0).getNumber();
+        }
+        String callId = mCallService.placeCall(call.getAccount(), number.getUriString(), !call.isVideoMuted());
+        if (callId == null || callId.isEmpty()) {
+            return null;
+        }
+        call.setCallID(callId);
+        Account account = mAccountService.getAccount(call.getAccount());
+        if (account.isRing()
+                || account.getDetailBoolean(ConfigKey.SRTP_ENABLE)
+                || account.getDetailBoolean(ConfigKey.TLS_ENABLE)) {
+            Log.i(TAG, "placeCall() call is secure");
+            SecureSipCall secureCall = new SecureSipCall(call, account.getDetail(ConfigKey.SRTP_KEY_EXCHANGE));
+            conf = new Conference(secureCall);
+        } else {
+            conf = new Conference(call);
+        }
+        conf.getParticipants().get(0).setContact(contact);
+        conv.addConference(conf);
+
+        return conf;
+    }
+
+    public void sendTextMessage(String account, Uri to, String txt) {
+        long id = mCallService.sendAccountTextMessage(account, to.getRawUriString(), txt);
+        Log.i(TAG, "sendAccountTextMessage " + txt + " got id " + id);
+        TextMessage message = new TextMessage(false, txt, to, null, account);
+        message.setID(id);
+        message.read();
+        mHistoryService.insertNewTextMessage(message);
+    }
+
+    public void sendTextMessage(Conference conf, String txt) {
+        mCallService.sendTextMessage(conf.getId(), txt);
+        SipCall call = conf.getParticipants().get(0);
+        TextMessage message = new TextMessage(false, txt, call.getNumberUri(), conf.getId(), call.getAccount());
+        message.read();
+        mHistoryService.insertNewTextMessage(message);
+    }
+
+    private void readTextMessage(TextMessage message) {
+        message.read();
+        HistoryText ht = new HistoryText(message);
+        mHistoryService.updateTextMessage(ht);
+    }
+
+    public void readConversation(Conversation conv) {
+        for (HistoryEntry h : conv.getRawHistory().values()) {
+            NavigableMap<Long, TextMessage> messages = h.getTextMessages();
+            for (TextMessage msg : messages.descendingMap().values()) {
+                if (msg.isRead()) {
+                    break;
+                }
+                readTextMessage(msg);
+            }
+        }
+        mNotificationService.cancelTextNotification(conv.getContact());
+        updateTextNotifications();
+    }
+
+    synchronized public void refreshConversations() {
+        Log.d(TAG, "refreshConversations()");
+        loadConversationsFromHistory();
+    }
+
+    public void updateTextNotifications() {
+        Log.d(TAG, "updateTextNotifications()");
+
+        for (Conversation conversation : mConversationMap.values()) {
+
+            if (conversation.isVisible()) {
+                mNotificationService.cancelTextNotification(conversation.getContact());
+                continue;
+            }
+
+            TreeMap<Long, TextMessage> texts = conversation.getUnreadTextMessages();
+            if (texts.isEmpty() || texts.lastEntry().getValue().isNotified()) {
+                continue;
+            } else {
+                mNotificationService.cancelTextNotification(conversation.getContact());
+            }
+
+            CallContact contact = conversation.getContact();
+            mNotificationService.showTextNotification(contact, conversation, texts);
+        }
+    }
+
+    public Conference getConference(String id) {
+        for (Conversation conv : mConversationMap.values()) {
+            Conference conf = conv.getConference(id);
+            if (conf != null) {
+                return conf;
+            }
+        }
+        return null;
+    }
+
+    @Override
+    public void update(Observable observable, ServiceEvent event) {
+
+        if (observable instanceof HistoryService && event != null) {
+            List<HistoryCall> historyCalls = (List<HistoryCall>) event.getEventInput(ServiceEvent.EventInput.HISTORY_CALLS, ArrayList.class);
+
+            for (HistoryCall call : historyCalls) {
+                CallContact contact = mContactService.findContact(call.getContactID(), call.getContactKey(), call.getNumber());
+
+                String key = contact.getIds().get(0);
+                if (mConversationMap.containsKey(key)) {
+                    mConversationMap.get(key).addHistoryCall(call);
+                } else {
+                    Conversation conversation = new Conversation(contact);
+                    conversation.addHistoryCall(call);
+                    mConversationMap.put(key, conversation);
+                }
+            }
+
+            List<HistoryText> historyTexts = (List<HistoryText>) event.getEventInput(ServiceEvent.EventInput.HISTORY_TEXTS, ArrayList.class);
+
+            for (HistoryText htext : historyTexts) {
+                CallContact contact = mContactService.findContact(htext.getContactID(), htext.getContactKey(), htext.getNumber());
+
+                TextMessage msg = new TextMessage(htext);
+                msg.setContact(contact);
+
+                String key = contact.getIds().get(0);
+                if (mConversationMap.containsKey(key)) {
+                    mConversationMap.get(key).addTextMessage(msg);
+                } else {
+                    Conversation c = new Conversation(contact);
+                    c.addTextMessage(msg);
+                    mConversationMap.put(key, c);
+                }
+            }
+
+            Map<String, ArrayList<String>> conferences = mConferenceService.getConferenceList();
+
+            for (Map.Entry<String, ArrayList<String>> conferenceEntry : conferences.entrySet()) {
+                Conference conference = new Conference(conferenceEntry.getKey());
+                for (String callId : conferenceEntry.getValue()) {
+                    SipCall call = getCall(callId).second;
+                    if (call == null) {
+                        call = new SipCall(callId, mCallService.getCallDetails(callId));
+                    }
+                    Account acc = mAccountService.getAccount(call.getAccount());
+                    if (acc.isRing()
+                            || acc.getDetailBoolean(ConfigKey.SRTP_ENABLE)
+                            || acc.getDetailBoolean(ConfigKey.TLS_ENABLE)) {
+                        call = new SecureSipCall(call, acc.getDetail(ConfigKey.SRTP_KEY_EXCHANGE));
+                    }
+                    conference.addParticipant(call);
+                }
+                List<SipCall> calls = conference.getParticipants();
+                if (calls.size() == 1) {
+                    SipCall call = calls.get(0);
+                    CallContact contact = mContactService.findContact(-1, null, call.getNumber());
+                    call.setContact(contact);
+
+                    Conversation conv = null;
+                    ArrayList<String> ids = contact.getIds();
+                    for (String id : ids) {
+                        conv = mConversationMap.get(id);
+                        if (conv != null) {
+                            break;
+                        }
+                    }
+                    if (conv != null) {
+                        conv.addConference(conference);
+                    } else {
+                        conv = new Conversation(contact);
+                        conv.addConference(conference);
+                        mConversationMap.put(ids.get(0), conv);
+                    }
+                }
+            }
+            for (Conversation conversation : mConversationMap.values()) {
+                Log.d(TAG, "Conversation : " + conversation.getContact().getId() + " " + conversation.getContact().getDisplayName() + " " + conversation.getLastNumberUsed(conversation.getLastAccountUsed()) + " " + conversation.getLastInteraction().toString());
+            }
+
+            for (CallContact contact : mContactService.getContacts()) {
+                String key = contact.getIds().get(0);
+                if (!mConversationMap.containsKey(key)) {
+                    mConversationMap.put(key, new Conversation(contact));
+                }
+            }
+
+            setChanged();
+            ServiceEvent e = new ServiceEvent(ServiceEvent.EventType.CONVERSATIONS_CHANGED);
+            notifyObservers(e);
+        }
+    }
+}
\ No newline at end of file
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 0d37336b71de5ba2d9f773ee68ed59fc0b1b9086..613e8c3cf8cb95fa96d3d6dd20a6fe8c07187dfb 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
@@ -26,7 +26,9 @@ import java.util.Collections;
 import java.util.Comparator;
 import java.util.Date;
 import java.util.HashMap;
+import java.util.Iterator;
 import java.util.Map;
+import java.util.NavigableMap;
 import java.util.Random;
 import java.util.Set;
 import java.util.TreeMap;
@@ -163,19 +165,26 @@ public class Conversation {
         return d;
     }
 
-    public void addHistoryCall(HistoryCall c) {
-        String accountId = c.getAccountID();
-        if (mHistory.containsKey(accountId))
-            mHistory.get(accountId).addHistoryCall(c, getContact());
+    public void addHistoryCall(HistoryCall call) {
+        if(getHistoryCalls().contains(call)){
+            return;
+        }
+        String accountId = call.getAccountID();
+        if (mHistory.containsKey(accountId)) {
+            mHistory.get(accountId).addHistoryCall(call, getContact());
+        }
         else {
-            HistoryEntry e = new HistoryEntry(accountId, getContact());
-            e.addHistoryCall(c, getContact());
-            mHistory.put(accountId, e);
+            HistoryEntry entry = new HistoryEntry(accountId, getContact());
+            entry.addHistoryCall(call, getContact());
+            mHistory.put(accountId, entry);
         }
-        mAggregateHistory.add(new ConversationElement(c));
+        mAggregateHistory.add(new ConversationElement(call));
     }
 
     public void addTextMessage(TextMessage txt) {
+        if(getTextMessages().contains(txt)){
+            return;
+        }
         if (txt.getContact() == null) {
             txt.setContact(getContact());
         }
@@ -238,16 +247,23 @@ public class Conversation {
 
     public Collection<TextMessage> getTextMessages(Date since) {
         TreeMap<Long, TextMessage> texts = new TreeMap<>();
+
         for (HistoryEntry h : mHistory.values()) {
-            texts.putAll(since == null ? h.getTextMessages() : h.getTextMessages(since.getTime()));
+            Map<Long, TextMessage> textMessages = since == null ? h.getTextMessages() : h.getTextMessages(since.getTime());
+            for (Map.Entry<Long, TextMessage> entry : textMessages.entrySet()) {
+                texts.put(entry.getKey(), entry.getValue());
+            }
         }
         return texts.values();
     }
 
     public Collection<HistoryCall> getHistoryCalls() {
         TreeMap<Long, HistoryCall> calls = new TreeMap<>();
+
         for (HistoryEntry historyEntry : mHistory.values()) {
-            calls.putAll(historyEntry.getCalls());
+            for (Map.Entry<Long, HistoryCall> entry : historyEntry.getCalls().descendingMap().entrySet()) {
+                calls.put(entry.getKey(), entry.getValue());
+            }
         }
         return calls.values();
     }
diff --git a/ring-android/libringclient/src/main/java/cx/ring/model/ServiceEvent.java b/ring-android/libringclient/src/main/java/cx/ring/model/ServiceEvent.java
index 7aa8b72c829db7f70753b0a0e89f15bb3a9e0435..fb099df249f150928cbb64f13496df621fbb91f5 100644
--- a/ring-android/libringclient/src/main/java/cx/ring/model/ServiceEvent.java
+++ b/ring-android/libringclient/src/main/java/cx/ring/model/ServiceEvent.java
@@ -57,7 +57,9 @@ public class ServiceEvent {
         MIGRATION_ENDED,
         INCOMING_TRUST_REQUEST,
         CONTACT_ADDED,
-        CONTACT_REMOVED
+        CONTACT_REMOVED,
+        CONVERSATIONS_CHANGED,
+        HISTORY_LOADED
     }
 
     public enum EventInput {
@@ -93,7 +95,9 @@ public class ServiceEvent {
         TIME,
         MESSAGE,
         CONFIRMED,
-        BANNED
+        BANNED,
+        HISTORY_CALLS,
+        HISTORY_TEXTS
     }
 
     private EventType mType;
diff --git a/ring-android/libringclient/src/main/java/cx/ring/services/HistoryService.java b/ring-android/libringclient/src/main/java/cx/ring/services/HistoryService.java
index a0222154256bc492bbfd845b333fc61ba174ab19..bb335fe1bca2dca631e95e09294fcdc248c1f1e6 100644
--- a/ring-android/libringclient/src/main/java/cx/ring/services/HistoryService.java
+++ b/ring-android/libringclient/src/main/java/cx/ring/services/HistoryService.java
@@ -29,12 +29,17 @@ import java.sql.SQLException;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
+import java.util.concurrent.ExecutorService;
+
+import javax.inject.Inject;
+import javax.inject.Named;
 
 import cx.ring.model.Conference;
 import cx.ring.model.Conversation;
 import cx.ring.model.HistoryCall;
 import cx.ring.model.HistoryEntry;
 import cx.ring.model.HistoryText;
+import cx.ring.model.ServiceEvent;
 import cx.ring.model.SipCall;
 import cx.ring.model.TextMessage;
 import cx.ring.utils.Log;
@@ -51,6 +56,10 @@ public abstract class HistoryService extends Observable {
 
     private static final String TAG = HistoryService.class.getSimpleName();
 
+    @Inject
+    @Named("ApplicationExecutor")
+    ExecutorService mApplicationExecutor;
+
     protected abstract ConnectionSource getConnectionSource();
 
     protected abstract Dao<HistoryCall, Integer> getCallHistoryDao();
@@ -122,13 +131,34 @@ public abstract class HistoryService extends Observable {
         return true;
     }
 
-    public List<HistoryCall> getAll() throws SQLException {
+    public void getCallAndTextAsync() throws SQLException {
+
+        mApplicationExecutor.submit(new Runnable() {
+            @Override
+            public void run() {
+                try {
+                    List<HistoryCall> historyCalls = getAll();
+                    List<HistoryText> historyTexts = getAllTextMessages();
+
+                    ServiceEvent event = new ServiceEvent(ServiceEvent.EventType.HISTORY_LOADED);
+                    event.addEventInput(ServiceEvent.EventInput.HISTORY_CALLS, historyCalls);
+                    event.addEventInput(ServiceEvent.EventInput.HISTORY_TEXTS, historyTexts);
+                    setChanged();
+                    notifyObservers(event);
+                } catch (SQLException e) {
+                    Log.e(TAG, "Can't load calls and texts", e);
+                }
+            }
+        });
+    }
+
+    private List<HistoryCall> getAll() throws SQLException {
         QueryBuilder<HistoryCall, Integer> queryBuilder = getCallHistoryDao().queryBuilder();
         queryBuilder.orderBy(HistoryCall.COLUMN_TIMESTAMP_START_NAME, true);
         return getCallHistoryDao().query(queryBuilder.prepare());
     }
 
-    public List<HistoryText> getAllTextMessages() throws SQLException {
+    private List<HistoryText> getAllTextMessages() throws SQLException {
         QueryBuilder<HistoryText, Integer> queryBuilder = getTextHistoryDao().queryBuilder();
         queryBuilder.orderBy(HistoryText.COLUMN_TIMESTAMP_NAME, true);
         return getTextHistoryDao().query(queryBuilder.prepare());