From fdf5ded0a4f17dba0becc66acdd006e5220beabe Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Adrien=20B=C3=A9raud?= <adrien.beraud@savoirfairelinux.com>
Date: Wed, 19 May 2021 15:47:56 -0400
Subject: [PATCH] swarm: various fixes and improvements

Change-Id: Ib36417ad4d68acfbb7d257c76ac739f6f02c4e01
---
 .../cx/ring/adapters/ConversationAdapter.java |  62 ++++--
 .../cx/ring/adapters/SmartListAdapter.java    |   2 +-
 .../cx/ring/adapters/SmartListDiffUtil.java   |   8 +-
 .../ring/client/ContactDetailsActivity.java   | 176 ++++++++++++---
 .../cx/ring/client/ConversationActivity.java  |   5 +-
 .../client/ConversationSelectionActivity.java |   4 +-
 .../java/cx/ring/fragments/CallFragment.java  |   3 +-
 .../ring/fragments/ContactPickerFragment.java |   4 +-
 .../ring/fragments/ConversationFragment.java  |  44 ++--
 .../cx/ring/fragments/LinkDeviceFragment.java |  18 +-
 .../cx/ring/fragments/SmartListFragment.java  | 132 +++++------
 .../ring/service/CallNotificationService.java |   8 +-
 .../java/cx/ring/service/DRingService.java    |   1 +
 .../cx/ring/services/DataTransferService.java |  83 ++++---
 .../services/DeviceRuntimeServiceImpl.java    |  20 +-
 .../services/NotificationServiceImpl.java     | 101 ++++-----
 .../SharedPreferencesServiceImpl.java         |   5 +
 .../cx/ring/services/VCardServiceImpl.java    |   5 +-
 .../cx/ring/settings/AccountFragment.java     |   7 +-
 .../java/cx/ring/share/ShareFragment.java     |  11 +-
 .../java/cx/ring/tv/call/TVCallFragment.java  |   3 +-
 .../ring/tv/cards/contacts/ContactCard.java   |   4 +-
 .../ring/tv/contact/TVContactPresenter.java   |   2 +-
 .../TVContactRequestDetailPresenter.java      |   2 +-
 .../conversation/TvConversationAdapter.java   |  35 ++-
 .../java/cx/ring/tv/main/HomeActivity.java    |   5 +-
 .../java/cx/ring/tv/main/MainFragment.java    |   2 +-
 .../java/cx/ring/utils/AndroidFileUtils.java  |   8 +-
 .../java/cx/ring/utils/BluetoothWrapper.java  |   3 +-
 .../java/cx/ring/utils/ClipboardHelper.java   |  30 +--
 .../java/cx/ring/utils/ConversationPath.java  |  23 +-
 .../java/cx/ring/utils/ResourceMapper.java    |  48 ++--
 .../java/cx/ring/views/AvatarDrawable.java    | 130 ++++++-----
 .../res/layout/activity_contact_details.xml   |  16 +-
 .../src/main/res/layout/frag_conversation.xml |   3 +-
 .../res/layout/item_contact_horizontal.xml    |  56 +++++
 .../app/src/main/res/values/strings.xml       |   1 +
 .../java/net/jami/call/CallPresenter.java     |  34 +--
 .../src/main/java/net/jami/call/CallView.java |   3 +-
 .../conversation/ConversationPresenter.java   |  66 +++---
 .../net/jami/facades/ConversationFacade.java  | 174 ++++++++-------
 .../src/main/java/net/jami/model/Account.java |  17 +-
 .../src/main/java/net/jami/model/Call.java    |  34 ++-
 .../src/main/java/net/jami/model/Contact.java |   4 +-
 .../java/net/jami/model/Conversation.java     | 207 +++++++++++-------
 .../java/net/jami/model/DataTransfer.java     |  16 +-
 .../main/java/net/jami/model/Interaction.java |  18 +-
 .../main/java/net/jami/model/TextMessage.java |   9 -
 .../net/jami/services/AccountService.java     | 152 ++++++++-----
 .../java/net/jami/services/CallService.java   |  11 +-
 .../net/jami/services/ContactService.java     |   7 +-
 .../jami/services/DeviceRuntimeService.java   |   2 +
 .../net/jami/services/HistoryService.java     |   2 +-
 .../jami/services/NotificationService.java    |  22 +-
 .../java/net/jami/services/VCardService.java  |   8 +-
 .../jami/smartlist/SmartListPresenter.java    |  43 ++--
 .../jami/smartlist/SmartListViewModel.java    |   9 +-
 .../main/java/net/jami/utils/VCardUtils.java  |   2 +-
 58 files changed, 1149 insertions(+), 761 deletions(-)
 create mode 100644 ring-android/app/src/main/res/layout/item_contact_horizontal.xml

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 cc69caea7..f6cf7b112 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
@@ -65,6 +65,18 @@ import com.bumptech.glide.load.resource.bitmap.CenterInside;
 import com.bumptech.glide.load.resource.bitmap.RoundedCorners;
 import com.bumptech.glide.request.target.DrawableImageViewTarget;
 
+import net.jami.conversation.ConversationPresenter;
+import net.jami.model.Account;
+import net.jami.model.Call;
+import net.jami.model.Contact;
+import net.jami.model.ContactEvent;
+import net.jami.model.DataTransfer;
+import net.jami.model.Interaction;
+import net.jami.model.Interaction.InteractionStatus;
+import net.jami.model.Interaction.InteractionType;
+import net.jami.model.TextMessage;
+import net.jami.utils.StringUtils;
+
 import java.io.File;
 import java.text.DateFormat;
 import java.util.ArrayList;
@@ -75,24 +87,11 @@ import java.util.concurrent.TimeUnit;
 
 import cx.ring.R;
 import cx.ring.client.MediaViewerActivity;
-import net.jami.conversation.ConversationPresenter;
 import cx.ring.fragments.ConversationFragment;
-import net.jami.model.Account;
-import net.jami.model.Contact;
-import net.jami.model.ContactEvent;
-import net.jami.model.DataTransfer;
-import net.jami.model.Interaction;
-import net.jami.model.Interaction.InteractionStatus;
-import net.jami.model.Interaction.InteractionType;
-import net.jami.model.Call;
-import net.jami.model.TextMessage;
-import cx.ring.service.DRingService;
-import cx.ring.utils.AndroidFileUtils;
 import cx.ring.utils.ContentUriHandler;
 import cx.ring.utils.GlideApp;
 import cx.ring.utils.GlideOptions;
 import cx.ring.utils.ResourceMapper;
-import net.jami.utils.StringUtils;
 import cx.ring.views.ConversationViewHolder;
 import io.reactivex.Observable;
 import io.reactivex.android.schedulers.AndroidSchedulers;
@@ -178,6 +177,7 @@ public class ConversationAdapter extends RecyclerView.Adapter<ConversationViewHo
             }
             for (int i = 0, n = mInteractions.size(); i<n; i++) {
                 if (mInteractions.get(i).getParentIds().contains(e.getMessageId())) {
+                    Log.w(TAG, "Adding message at " + i + " previous count " + n);
                     mInteractions.add(i, e);
                     notifyItemInserted(i);
                     return i == n-1;
@@ -194,6 +194,7 @@ public class ConversationAdapter extends RecyclerView.Adapter<ConversationViewHo
     }
 
     public void update(Interaction e) {
+        Log.w(TAG, "update " + e.getMessageId());
         if (!e.isIncoming() && e.getStatus() == InteractionStatus.SUCCESS) {
             notifyItemChanged(lastDeliveredPosition);
         }
@@ -207,18 +208,33 @@ public class ConversationAdapter extends RecyclerView.Adapter<ConversationViewHo
     }
 
     public void remove(Interaction e) {
-        for (int i = mInteractions.size() - 1; i >= 0; i--) {
-            Interaction element = mInteractions.get(i);
-            if (e.getId() == element.getId()) {
-                mInteractions.remove(i);
-                notifyItemRemoved(i);
-                if (i > 0) {
-                    notifyItemChanged(i - 1);
+        if (e.getMessageId() != null) {
+            for (int i = mInteractions.size() - 1; i >= 0; i--) {
+                if (e.getMessageId().equals(mInteractions.get(i).getMessageId())) {
+                    mInteractions.remove(i);
+                    notifyItemRemoved(i);
+                    if (i > 0) {
+                        notifyItemChanged(i - 1);
+                    }
+                    if (i != mInteractions.size()) {
+                        notifyItemChanged(i);
+                    }
+                    break;
                 }
-                if (i != mInteractions.size()) {
-                    notifyItemChanged(i);
+            }
+        } else {
+            for (int i = mInteractions.size() - 1; i >= 0; i--) {
+                if (e.getId() == mInteractions.get(i).getId()) {
+                    mInteractions.remove(i);
+                    notifyItemRemoved(i);
+                    if (i > 0) {
+                        notifyItemChanged(i - 1);
+                    }
+                    if (i != mInteractions.size()) {
+                        notifyItemChanged(i);
+                    }
+                    break;
                 }
-                break;
             }
         }
     }
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 d4e973218..4d8c77179 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
@@ -106,7 +106,7 @@ public class SmartListAdapter extends RecyclerView.Adapter<SmartListViewHolder>
     public void update(SmartListViewModel smartListViewModel) {
         for (int i = 0; i < mSmartListViewModels.size(); i++) {
             SmartListViewModel old = mSmartListViewModels.get(i);
-            if (old.getContact() == smartListViewModel.getContact()) {
+            if (old.getContacts() == smartListViewModel.getContacts()) {
                 mSmartListViewModels.set(i, smartListViewModel);
                 notifyItemChanged(i);
                 return;
diff --git a/ring-android/app/src/main/java/cx/ring/adapters/SmartListDiffUtil.java b/ring-android/app/src/main/java/cx/ring/adapters/SmartListDiffUtil.java
index 52a951677..f26aff1fb 100644
--- a/ring-android/app/src/main/java/cx/ring/adapters/SmartListDiffUtil.java
+++ b/ring-android/app/src/main/java/cx/ring/adapters/SmartListDiffUtil.java
@@ -51,11 +51,11 @@ public class SmartListDiffUtil extends DiffUtil.Callback {
         SmartListViewModel newItem = mNewList.get(newItemPosition);
         if (newItem.getHeaderTitle() != oldItem.getHeaderTitle())
             return false;
-        if (newItem.getContact() != oldItem.getContact()) {
-            if (newItem.getContact().size() != oldItem.getContact().size())
+        if (newItem.getContacts() != oldItem.getContacts()) {
+            if (newItem.getContacts().size() != oldItem.getContacts().size())
                 return false;
-            for (int i = 0; i < newItem.getContact().size(); i++) {
-                if (newItem.getContact().get(i) != oldItem.getContact().get(i))
+            for (int i = 0; i < newItem.getContacts().size(); i++) {
+                if (newItem.getContacts().get(i) != oldItem.getContacts().get(i))
                     return false;
             }
         }
diff --git a/ring-android/app/src/main/java/cx/ring/client/ContactDetailsActivity.java b/ring-android/app/src/main/java/cx/ring/client/ContactDetailsActivity.java
index a5d520c14..71dc2eb04 100644
--- a/ring-android/app/src/main/java/cx/ring/client/ContactDetailsActivity.java
+++ b/ring-android/app/src/main/java/cx/ring/client/ContactDetailsActivity.java
@@ -26,48 +26,53 @@ import android.content.Intent;
 import android.content.SharedPreferences;
 import android.content.res.ColorStateList;
 import android.graphics.Color;
+import android.graphics.drawable.Drawable;
 import android.os.Bundle;
-
-import com.google.android.material.appbar.CollapsingToolbarLayout;
-import com.google.android.material.dialog.MaterialAlertDialogBuilder;
-import com.google.android.material.floatingactionbutton.FloatingActionButton;
-import com.google.android.material.snackbar.Snackbar;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Toast;
 
 import androidx.annotation.DrawableRes;
 import androidx.annotation.NonNull;
 import androidx.annotation.StringRes;
 import androidx.appcompat.app.AppCompatActivity;
+import androidx.core.view.ViewCompat;
+import androidx.recyclerview.widget.RecyclerView;
 
-import android.util.Log;
-import android.view.LayoutInflater;
-import android.view.ViewGroup;
-import android.widget.Toast;
+import com.google.android.material.appbar.CollapsingToolbarLayout;
+import com.google.android.material.dialog.MaterialAlertDialogBuilder;
+import com.google.android.material.floatingactionbutton.FloatingActionButton;
+import com.google.android.material.snackbar.Snackbar;
+
+import net.jami.facades.ConversationFacade;
+import net.jami.model.Call;
+import net.jami.model.Conference;
+import net.jami.model.Contact;
+import net.jami.model.Conversation;
+import net.jami.model.Uri;
+import net.jami.services.AccountService;
+import net.jami.services.NotificationService;
 
 import java.util.ArrayList;
+import java.util.List;
 
 import javax.inject.Inject;
 import javax.inject.Singleton;
 
-import androidx.core.view.ViewCompat;
-import androidx.recyclerview.widget.RecyclerView;
-
 import cx.ring.R;
 import cx.ring.application.JamiApplication;
 import cx.ring.views.AvatarFactory;
 import cx.ring.databinding.ActivityContactDetailsBinding;
 import cx.ring.databinding.ItemContactActionBinding;
-import net.jami.facades.ConversationFacade;
+import cx.ring.databinding.ItemContactHorizontalBinding;
 import cx.ring.fragments.CallFragment;
 import cx.ring.fragments.ConversationFragment;
-import net.jami.model.Contact;
-import net.jami.model.Conference;
-import net.jami.model.Conversation;
-import net.jami.model.Call;
-import net.jami.model.Uri;
-import net.jami.services.AccountService;
-import net.jami.services.NotificationService;
+import cx.ring.services.SharedPreferencesServiceImpl;
 import cx.ring.utils.ConversationPath;
 import cx.ring.views.AvatarDrawable;
+import io.reactivex.Single;
 import io.reactivex.android.schedulers.AndroidSchedulers;
 import io.reactivex.disposables.CompositeDisposable;
 
@@ -93,6 +98,8 @@ public class ContactDetailsActivity extends AppCompatActivity {
     static class ContactAction {
         @DrawableRes
         final int icon;
+        final Single<Drawable> drawable;
+
         final CharSequence title;
         final IContactAction callback;
 
@@ -104,6 +111,7 @@ public class ContactDetailsActivity extends AppCompatActivity {
             iconTint = tint;
             title = t;
             callback = cb;
+            drawable = null;
         }
 
         ContactAction(@DrawableRes int i, CharSequence t, IContactAction cb) {
@@ -111,6 +119,14 @@ public class ContactDetailsActivity extends AppCompatActivity {
             iconTint = Color.BLACK;
             title = t;
             callback = cb;
+            drawable = null;
+        }
+        ContactAction(Single<Drawable> d, CharSequence t, IContactAction cb) {
+            drawable = d;
+            icon = 0;
+            iconTint = Color.BLACK;
+            title = t;
+            callback = cb;
         }
 
         void setIconTint(int tint) {
@@ -125,10 +141,12 @@ public class ContactDetailsActivity extends AppCompatActivity {
     static class ContactActionView extends RecyclerView.ViewHolder {
         final ItemContactActionBinding binding;
         IContactAction callback;
+        final CompositeDisposable disposable = new CompositeDisposable();
 
-        ContactActionView(@NonNull ItemContactActionBinding b) {
+        ContactActionView(@NonNull ItemContactActionBinding b, CompositeDisposable parentDisposable) {
             super(b.getRoot());
             binding = b;
+            parentDisposable.add(disposable);
             itemView.setOnClickListener(view -> {
                 try {
                     if (callback != null)
@@ -142,34 +160,112 @@ public class ContactDetailsActivity extends AppCompatActivity {
 
     private static class ContactActionAdapter extends RecyclerView.Adapter<ContactActionView> {
         private final ArrayList<ContactAction> actions = new ArrayList<>();
+        private final CompositeDisposable disposable;
+
+        private ContactActionAdapter(CompositeDisposable disposable) {
+            this.disposable = disposable;
+        }
 
         @NonNull
         @Override
         public ContactActionView onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
             LayoutInflater layoutInflater = LayoutInflater.from(parent.getContext());
             ItemContactActionBinding itemBinding = ItemContactActionBinding.inflate(layoutInflater, parent, false);
-            return new ContactActionView(itemBinding);
+            return new ContactActionView(itemBinding, disposable);
         }
 
         @Override
         public void onBindViewHolder(@NonNull ContactActionView holder, int position) {
             ContactAction action = actions.get(position);
-            holder.binding.actionIcon.setBackgroundResource(action.icon);
-            holder.binding.actionIcon.setText(action.iconSymbol);
-            if (action.iconTint != Color.BLACK)
-                ViewCompat.setBackgroundTintList(holder.binding.actionIcon, ColorStateList.valueOf(action.iconTint));
+            holder.disposable.clear();
+            if (action.drawable != null) {
+                holder.disposable.add(action.drawable.subscribe(holder.binding.actionIcon::setBackground));
+            } else {
+                holder.binding.actionIcon.setBackgroundResource(action.icon);
+                holder.binding.actionIcon.setText(action.iconSymbol);
+                if (action.iconTint != Color.BLACK)
+                    ViewCompat.setBackgroundTintList(holder.binding.actionIcon, ColorStateList.valueOf(action.iconTint));
+            }
             holder.binding.actionTitle.setText(action.title);
             holder.callback = action.callback;
         }
 
+        @Override
+        public void onViewRecycled(@NonNull ContactActionView holder) {
+            holder.disposable.clear();
+            holder.binding.actionIcon.setBackground(null);
+        }
+
         @Override
         public int getItemCount() {
             return actions.size();
         }
     }
 
-    private final ContactActionAdapter adapter = new ContactActionAdapter();
+    static class ContactView extends RecyclerView.ViewHolder {
+        final ItemContactHorizontalBinding binding;
+        IContactAction callback;
+        final CompositeDisposable disposable = new CompositeDisposable();
+
+        ContactView(@NonNull ItemContactHorizontalBinding b, CompositeDisposable parentDisposable) {
+            super(b.getRoot());
+            binding = b;
+            parentDisposable.add(disposable);
+            itemView.setOnClickListener(view -> {
+                try {
+                    if (callback != null)
+                        callback.onAction();
+                } catch (Exception e) {
+                    Log.w(TAG, "Error performing action", e);
+                }
+            });
+        }
+    }
+    private static class ContactViewAdapter extends RecyclerView.Adapter<ContactView> {
+        private final List<Contact> contacts;
+        private final CompositeDisposable disposable;
+        interface ContactCallback {
+            void onContactClicked(Contact contact);
+        }
+        private final ContactCallback callback;
+
+        private ContactViewAdapter(CompositeDisposable disposable, List<Contact> contacts, ContactCallback cb) {
+            this.disposable = disposable;
+            this.contacts = contacts;
+            this.callback = cb;
+        }
+
+        @NonNull
+        @Override
+        public ContactView onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+            LayoutInflater layoutInflater = LayoutInflater.from(parent.getContext());
+            ItemContactHorizontalBinding itemBinding = ItemContactHorizontalBinding.inflate(layoutInflater, parent, false);
+            return new ContactView(itemBinding, disposable);
+        }
+
+        @Override
+        public void onBindViewHolder(@NonNull ContactView holder, int position) {
+            Contact contact = contacts.get(position);
+            holder.disposable.clear();
+            holder.disposable.add(AvatarFactory.getAvatar(holder.itemView.getContext(), contact, false).subscribe(holder.binding.photo::setImageDrawable));
+            holder.binding.displayName.setText(contact.isUser() ? holder.itemView.getContext().getText(R.string.conversation_info_contact_you) : contact.getDisplayName());
+            holder.itemView.setOnClickListener(v -> callback.onContactClicked(contact));
+        }
+
+        @Override
+        public void onViewRecycled(@NonNull ContactView holder) {
+            holder.disposable.clear();
+            holder.binding.photo.setImageDrawable(null);
+        }
+
+        @Override
+        public int getItemCount() {
+            return contacts.size();
+        }
+    }
+
     private final CompositeDisposable mDisposableBag = new CompositeDisposable();
+    private final ContactActionAdapter adapter = new ContactActionAdapter(mDisposableBag);
 
     private ContactAction colorAction;
     private ContactAction symbolAction;
@@ -194,7 +290,7 @@ public class ContactDetailsActivity extends AppCompatActivity {
 
         setSupportActionBar(findViewById(R.id.toolbar));
 
-        FloatingActionButton fab = findViewById(R.id.fab);
+        FloatingActionButton fab = findViewById(R.id.sendMessage);
         fab.setOnClickListener(view -> goToConversationActivity(mConversation.getAccountId(), mConversation.getUri()));
 
         colorActionPosition = 1;
@@ -205,7 +301,7 @@ public class ContactDetailsActivity extends AppCompatActivity {
                 .observeOn(AndroidSchedulers.mainThread())
                 .subscribe(conversation -> {
                     mConversation = conversation;
-                    mPreferences = getSharedPreferences(conversation.getAccountId() + "_" + conversation.getUri().getUri(), Context.MODE_PRIVATE);
+                    mPreferences = SharedPreferencesServiceImpl.getConversationPreferences(this, conversation.getAccountId(), conversation.getUri());
                     binding.contactImage.setImageDrawable(
                             new AvatarDrawable.Builder()
                                     .withConversation(conversation)
@@ -221,10 +317,11 @@ public class ContactDetailsActivity extends AppCompatActivity {
 
                     @StringRes int infoString = conversation.isSwarm()
                             ? (conversation.getMode() == Conversation.Mode.OneToOne
-                                ? R.string.conversation_type_private
-                                : R.string.conversation_type_group)
+                            ? R.string.conversation_type_private
+                            : R.string.conversation_type_group)
                             : R.string.conversation_type_contact;
-                    adapter.actions.add(new ContactAction(R.drawable.baseline_info_24, getText(infoString), () -> {}));
+                    adapter.actions.add(new ContactAction(R.drawable.baseline_info_24, getText(infoString), () -> {
+                    }));
 
                     colorAction = new ContactAction(R.drawable.item_color_background, 0, getText(R.string.conversation_preference_color), () -> {
                         ColorChooserBottomSheet frag = new ColorChooserBottomSheet();
@@ -288,7 +385,8 @@ public class ContactDetailsActivity extends AppCompatActivity {
                                         .create()
                                         .show()));
                     }
-                    contactAction = new ContactAction(R.drawable.baseline_person_24, conversation.getUriTitle(), () -> {
+                    String conversationUri = conversation.isSwarm() ? conversation.getUri().toString() : conversation.getUriTitle();
+                    contactAction = new ContactAction(conversation.isSwarm() ? R.drawable.baseline_group_24 : R.drawable.baseline_person_24, conversationUri, () -> {
                         ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
                         if (clipboard != null) {
                             clipboard.setPrimaryClip(ClipData.newPlainText(getText(R.string.clip_contact_uri), path.getConversationId()));
@@ -297,6 +395,18 @@ public class ContactDetailsActivity extends AppCompatActivity {
                     });
                     adapter.actions.add(contactAction);
                     binding.contactActionList.setAdapter(adapter);
+
+                    binding.contactList.setVisibility(conversation.isSwarm() ? View.VISIBLE : View.GONE);
+                    if (conversation.isSwarm()) {
+                        binding.contactList.setAdapter(new ContactViewAdapter(mDisposableBag, conversation.getContacts(), contact -> {
+                            ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
+                            if (clipboard != null) {
+                                String toCopy = contact.getUri().getRawUriString();
+                                clipboard.setPrimaryClip(ClipData.newPlainText(getText(R.string.clip_contact_uri), toCopy));
+                                Snackbar.make(binding.getRoot(), getString(R.string.conversation_action_copied_peer_number_clipboard, toCopy), Snackbar.LENGTH_LONG).show();
+                            }
+                        }));
+                    }
                 }));
     }
 
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 0474329f7..d10cc5430 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
@@ -24,14 +24,11 @@ import android.content.Intent;
 import android.os.Bundle;
 import android.view.KeyEvent;
 import android.view.Menu;
-import android.view.View;
 
 import androidx.annotation.ColorInt;
 import androidx.annotation.NonNull;
 import androidx.appcompat.app.ActionBar;
 import androidx.appcompat.app.AppCompatActivity;
-import androidx.coordinatorlayout.widget.CoordinatorLayout;
-import androidx.core.view.ViewCompat;
 
 import cx.ring.R;
 import cx.ring.application.JamiApplication;
@@ -85,7 +82,7 @@ public class ConversationActivity extends AppCompatActivity implements Colorable
             mConversationFragment.setArguments(bundle);
             getSupportFragmentManager().beginTransaction()
                     .replace(R.id.main_frame, mConversationFragment, null)
-                    .commit();
+                    .commitNow();
         }
         if (Intent.ACTION_SEND.equals(action) || Intent.ACTION_SEND_MULTIPLE.equals(action) || Intent.ACTION_VIEW.equals(action)) {
             mPendingIntent = intent;
diff --git a/ring-android/app/src/main/java/cx/ring/client/ConversationSelectionActivity.java b/ring-android/app/src/main/java/cx/ring/client/ConversationSelectionActivity.java
index f9062b4ee..3f464ecfc 100644
--- a/ring-android/app/src/main/java/cx/ring/client/ConversationSelectionActivity.java
+++ b/ring-android/app/src/main/java/cx/ring/client/ConversationSelectionActivity.java
@@ -117,11 +117,11 @@ public class ConversationSelectionActivity extends AppCompatActivity {
                         return vm;
                     List<SmartListViewModel> filteredVms = new ArrayList<>(vm.size());
                     models: for (SmartListViewModel v : vm) {
-                        List<Contact> contacts = v.getContact();
+                        List<Contact> contacts = v.getContacts();
                         if (contacts.size() != 1)
                             continue;
                         for (Call call : conf.getParticipants()) {
-                            if (call.getContact() == v.getContact().get(0)) {
+                            if (call.getContact() == v.getContacts().get(0)) {
                                 continue models;
                             }
                         }
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 6e84813e1..687e76f29 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
@@ -92,6 +92,7 @@ import net.jami.daemon.JamiService;
 import net.jami.model.Call;
 import net.jami.model.Conference;
 import net.jami.model.Contact;
+import net.jami.model.Uri;
 import net.jami.services.DeviceRuntimeService;
 import net.jami.services.HardwareService;
 import net.jami.services.NotificationService;
@@ -1237,7 +1238,7 @@ public class CallFragment extends BaseSupportFragment<CallPresenter> implements
     }
 
     @Override
-    public void goToConversation(String accountId, String conversationId) {
+    public void goToConversation(String accountId, Uri conversationId) {
         Context context = requireContext();
         if (DeviceUtils.isTablet(context)) {
             startActivity(new Intent(DRingService.ACTION_CONV_ACCEPT, ConversationPath.toUri(accountId, conversationId), context, HomeActivity.class));
diff --git a/ring-android/app/src/main/java/cx/ring/fragments/ContactPickerFragment.java b/ring-android/app/src/main/java/cx/ring/fragments/ContactPickerFragment.java
index 19cd9c897..9e61d435c 100644
--- a/ring-android/app/src/main/java/cx/ring/fragments/ContactPickerFragment.java
+++ b/ring-android/app/src/main/java/cx/ring/fragments/ContactPickerFragment.java
@@ -107,7 +107,7 @@ public class ContactPickerFragment extends BottomSheetDialogFragment {
                 adapter.update(item);
 
                 Runnable remover = () -> {
-                    mCurrentSelection.remove(item.getContact().get(0));
+                    mCurrentSelection.remove(item.getContacts().get(0));
                     if (mCurrentSelection.isEmpty())
                         binding.createGroupBtn.setEnabled(false);
                     item.setChecked(false);
@@ -118,7 +118,7 @@ public class ContactPickerFragment extends BottomSheetDialogFragment {
                 };
 
                 if (checked) {
-                    if (mCurrentSelection.add(item.getContact().get(0))) {
+                    if (mCurrentSelection.add(item.getContacts().get(0))) {
                         Chip chip = new Chip(requireContext(), null, R.style.Widget_MaterialComponents_Chip_Entry);
                         chip.setText(item.getContactName());
                         chip.setChipIcon(new AvatarDrawable.Builder()
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 1b4cb7af1..14dfae4d0 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
@@ -102,6 +102,7 @@ import cx.ring.service.DRingService;
 import cx.ring.services.LocationSharingService;
 import net.jami.services.NotificationService;
 import cx.ring.services.NotificationServiceImpl;
+import cx.ring.services.SharedPreferencesServiceImpl;
 import cx.ring.utils.ActionHelper;
 import cx.ring.utils.AndroidFileUtils;
 import cx.ring.utils.ContentUriHandler;
@@ -248,7 +249,7 @@ public class ConversationFragment extends BaseSupportFragment<ConversationPresen
         View layout = binding.conversationLayout;
 
         // remove action bar height for tablet layout
-        if (DeviceUtils.isTablet(getContext())) {
+        if (DeviceUtils.isTablet(layout.getContext())) {
             layout.setPadding(layout.getPaddingLeft(), 0, layout.getPaddingRight(), layout.getPaddingBottom());
         }
 
@@ -799,7 +800,7 @@ public class ConversationFragment extends BaseSupportFragment<ConversationPresen
         try {
             fileUri = ContentUriHandler.getUriForFile(c, ContentUriHandler.AUTHORITY_FILES, path);
         } catch (IllegalArgumentException e) {
-            Log.e("File Selector", "The selected file can't be shared: " + path.getName());
+            Log.e(TAG, "The selected file can't be shared: " + path.getName());
         }
         if (fileUri != null) {
             Intent sendIntent = new Intent();
@@ -861,28 +862,28 @@ public class ConversationFragment extends BaseSupportFragment<ConversationPresen
 
     @Override
     public boolean onOptionsItemSelected(MenuItem item) {
-        switch (item.getItemId()) {
-            case android.R.id.home:
-                startActivity(new Intent(getActivity(), HomeActivity.class));
-                return true;
-            case R.id.conv_action_audiocall:
-                presenter.goToCall(true);
-                return true;
-            case R.id.conv_action_videocall:
-                presenter.goToCall(false);
-                return true;
-            case R.id.conv_contact_details:
-                presenter.openContact();
-                return true;
-            default:
-                return super.onOptionsItemSelected(item);
+        int itemId = item.getItemId();
+        if (itemId == android.R.id.home) {
+            startActivity(new Intent(getActivity(), HomeActivity.class));
+            return true;
+        } else if (itemId == R.id.conv_action_audiocall) {
+            presenter.goToCall(true);
+            return true;
+        } else if (itemId == R.id.conv_action_videocall) {
+            presenter.goToCall(false);
+            return true;
+        } else if (itemId == R.id.conv_contact_details) {
+            presenter.openContact();
+            return true;
         }
+        return super.onOptionsItemSelected(item);
     }
 
     @Override
     protected void initPresenter(ConversationPresenter presenter) {
         ConversationPath path = ConversationPath.fromBundle(getArguments());
         mIsBubble = getArguments().getBoolean(NotificationServiceImpl.EXTRA_BUBBLE);
+        Log.w(TAG, "initPresenter " + path);
         if (path == null)
             return;
 
@@ -890,7 +891,7 @@ public class ConversationFragment extends BaseSupportFragment<ConversationPresen
         mAdapter = new ConversationAdapter(this, presenter);
         presenter.init(uri, path.getAccountId());
         try {
-            mPreferences = requireActivity().getSharedPreferences(path.getAccountId() + "_" + uri.getUri(), Context.MODE_PRIVATE);
+            mPreferences = SharedPreferencesServiceImpl.getConversationPreferences(requireContext(), path.getAccountId(), uri);
             mPreferences.registerOnSharedPreferenceChangeListener(this);
             presenter.setConversationColor(mPreferences.getInt(KEY_PREFERENCE_CONVERSATION_COLOR, getResources().getColor(R.color.color_primary_light)));
             presenter.setConversationSymbol(mPreferences.getString(KEY_PREFERENCE_CONVERSATION_SYMBOL, getResources().getText(R.string.conversation_default_emoji).toString()));
@@ -955,7 +956,11 @@ public class ConversationFragment extends BaseSupportFragment<ConversationPresen
                     .observeOn(AndroidSchedulers.mainThread())
                     .subscribe(avatar -> {
                         mParticipantAvatars.put(contactKey, (AvatarDrawable) avatar);
-                        mSmallParticipantAvatars.put(contactKey, new AvatarDrawable((AvatarDrawable) avatar));
+                        mSmallParticipantAvatars.put(contactKey, new AvatarDrawable.Builder()
+                                .withContact(contact)
+                                .withCircleCrop(true)
+                                .withPresence(false)
+                                .build(requireContext()));
                         mAdapter.setPhoto();
                     }));
         }
@@ -996,6 +1001,7 @@ public class ConversationFragment extends BaseSupportFragment<ConversationPresen
 
     @Override
     public void goToHome() {
+
         if (getActivity() instanceof ConversationActivity) {
             getActivity().finish();
         }
diff --git a/ring-android/app/src/main/java/cx/ring/fragments/LinkDeviceFragment.java b/ring-android/app/src/main/java/cx/ring/fragments/LinkDeviceFragment.java
index c2a7e5806..5e04bac96 100644
--- a/ring-android/app/src/main/java/cx/ring/fragments/LinkDeviceFragment.java
+++ b/ring-android/app/src/main/java/cx/ring/fragments/LinkDeviceFragment.java
@@ -43,13 +43,14 @@ import androidx.coordinatorlayout.widget.CoordinatorLayout;
 import com.google.android.material.bottomsheet.BottomSheetBehavior;
 import com.google.android.material.dialog.MaterialAlertDialogBuilder;
 
-import cx.ring.R;
-import cx.ring.account.AccountEditionFragment;
 import net.jami.account.LinkDevicePresenter;
 import net.jami.account.LinkDeviceView;
+import net.jami.model.Account;
+
+import cx.ring.R;
+import cx.ring.account.AccountEditionFragment;
 import cx.ring.application.JamiApplication;
 import cx.ring.databinding.FragLinkDeviceBinding;
-import net.jami.model.Account;
 import cx.ring.mvp.BaseBottomSheetFragment;
 import cx.ring.utils.DeviceUtils;
 import cx.ring.utils.KeyboardVisibilityManager;
@@ -76,10 +77,8 @@ public class LinkDeviceFragment extends BaseBottomSheetFragment<LinkDevicePresen
     @Override
     public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
         super.onCreateView(inflater, container, savedInstanceState);
-
-        ((JamiApplication) getActivity().getApplication()).getInjectionComponent().inject(this);
+        ((JamiApplication) requireActivity().getApplication()).getInjectionComponent().inject(this);
         mBinding = FragLinkDeviceBinding.inflate(inflater, container, false);
-
         return mBinding.getRoot();
     }
 
@@ -110,7 +109,7 @@ public class LinkDeviceFragment extends BaseBottomSheetFragment<LinkDevicePresen
     public Dialog onCreateDialog(Bundle savedInstanceState) {
         final Dialog dialog = super.onCreateDialog(savedInstanceState);
         dialog.setOnShowListener(dialogINterface -> {
-            if (DeviceUtils.isTablet(getContext())) {
+            if (DeviceUtils.isTablet(requireContext())) {
                 dialog.getWindow().setLayout(
                         ViewGroup.LayoutParams.WRAP_CONTENT,
                         ViewGroup.LayoutParams.MATCH_PARENT);
@@ -122,7 +121,7 @@ public class LinkDeviceFragment extends BaseBottomSheetFragment<LinkDevicePresen
     @Override
     public void onResume() {
         super.onResume();
-        addGlobalLayoutListener(getView());
+        addGlobalLayoutListener(requireView());
     }
 
     private void addGlobalLayoutListener(final View view) {
@@ -145,12 +144,11 @@ public class LinkDeviceFragment extends BaseBottomSheetFragment<LinkDevicePresen
     }
 
     private BottomSheetBehavior<?> getBottomSheetBehaviour() {
-        CoordinatorLayout.LayoutParams layoutParams = (CoordinatorLayout.LayoutParams) ((View) getView().getParent()).getLayoutParams();
+        CoordinatorLayout.LayoutParams layoutParams = (CoordinatorLayout.LayoutParams) ((View) requireView().getParent()).getLayoutParams();
         CoordinatorLayout.Behavior<?> behavior = layoutParams.getBehavior();
         if (behavior instanceof BottomSheetBehavior) {
             return (BottomSheetBehavior<?>) behavior;
         }
-
         return null;
     }
 
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 d3ef98517..4bb7dd86f 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
@@ -27,21 +27,6 @@ import android.content.Intent;
 import android.net.Uri;
 import android.os.Build;
 import android.os.Bundle;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-import com.google.android.material.dialog.MaterialAlertDialogBuilder;
-import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton;
-import com.google.android.material.snackbar.Snackbar;
-
-import androidx.recyclerview.widget.DefaultItemAnimator;
-import androidx.recyclerview.widget.LinearLayoutManager;
-import androidx.recyclerview.widget.RecyclerView;
-import androidx.appcompat.widget.SearchView;
-import androidx.appcompat.widget.Toolbar;
-
-import android.os.Handler;
 import android.text.InputType;
 import android.util.Log;
 import android.util.TypedValue;
@@ -55,6 +40,24 @@ import android.view.inputmethod.EditorInfo;
 import android.widget.EditText;
 import android.widget.RelativeLayout;
 
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.widget.SearchView;
+import androidx.appcompat.widget.Toolbar;
+import androidx.recyclerview.widget.DefaultItemAnimator;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.google.android.material.dialog.MaterialAlertDialogBuilder;
+import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton;
+import com.google.android.material.snackbar.Snackbar;
+
+import net.jami.model.Conversation;
+import net.jami.services.AccountService;
+import net.jami.smartlist.SmartListPresenter;
+import net.jami.smartlist.SmartListView;
+import net.jami.smartlist.SmartListViewModel;
+
 import java.util.List;
 
 import javax.inject.Inject;
@@ -65,12 +68,7 @@ import cx.ring.application.JamiApplication;
 import cx.ring.client.CallActivity;
 import cx.ring.client.HomeActivity;
 import cx.ring.databinding.FragSmartlistBinding;
-import net.jami.model.Conversation;
 import cx.ring.mvp.BaseSupportFragment;
-import net.jami.services.AccountService;
-import net.jami.smartlist.SmartListPresenter;
-import net.jami.smartlist.SmartListView;
-import net.jami.smartlist.SmartListViewModel;
 import cx.ring.utils.ActionHelper;
 import cx.ring.utils.ClipboardHelper;
 import cx.ring.utils.ConversationPath;
@@ -80,12 +78,9 @@ import cx.ring.viewholders.SmartListViewHolder;
 public class SmartListFragment extends BaseSupportFragment<SmartListPresenter> implements SearchView.OnQueryTextListener,
         SmartListViewHolder.SmartListListeners,
         Conversation.ConversationActionCallback,
-        ClipboardHelper.ClipboardHelperCallback,
-        SmartListView
-{
+        SmartListView {
     private static final String TAG = SmartListFragment.class.getSimpleName();
     private static final String STATE_LOADING = TAG + ".STATE_LOADING";
-    public static final String KEY_ACCOUNT_ID = "accountId";
 
     private static final int SCROLL_DIRECTION_UP = -1;
 
@@ -148,7 +143,7 @@ public class SmartListFragment extends BaseSupportFragment<SmartListPresenter> i
     @Override
     public void onStart() {
         super.onStart();
-        Activity  activity = getActivity();
+        Activity activity = getActivity();
         Intent intent = activity == null ? null : activity.getIntent();
         if (intent != null)
             handleIntent(intent);
@@ -177,30 +172,29 @@ public class SmartListFragment extends BaseSupportFragment<SmartListPresenter> i
 
     @Override
     public boolean onOptionsItemSelected(MenuItem item) {
-        switch (item.getItemId()) {
-            case R.id.menu_contact_search:
-                mSearchView.setInputType(EditorInfo.TYPE_CLASS_TEXT
-                        | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS
-                );
-                return false;
-            case R.id.menu_contact_dial:
-                if (mSearchView.getInputType() == EditorInfo.TYPE_CLASS_PHONE) {
-                    mSearchView.setInputType(EditorInfo.TYPE_CLASS_TEXT);
-                    mDialpadMenuItem.setIcon(R.drawable.baseline_dialpad_24);
-                } else {
-                    mSearchView.setInputType(EditorInfo.TYPE_CLASS_PHONE);
-                    mDialpadMenuItem.setIcon(R.drawable.baseline_keyboard_24);
-                }
-                return true;
-            case R.id.menu_settings:
-                ((HomeActivity) getActivity()).goToSettings();
-                return true;
-            case R.id.menu_about:
-                ((HomeActivity) getActivity()).goToAbout();
-                return true;
-            default:
-                return false;
+        int itemId = item.getItemId();
+        if (itemId == R.id.menu_contact_search) {
+            mSearchView.setInputType(EditorInfo.TYPE_CLASS_TEXT
+                    | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS
+            );
+            return false;
+        } else if (itemId == R.id.menu_contact_dial) {
+            if (mSearchView.getInputType() == EditorInfo.TYPE_CLASS_PHONE) {
+                mSearchView.setInputType(EditorInfo.TYPE_CLASS_TEXT);
+                mDialpadMenuItem.setIcon(R.drawable.baseline_dialpad_24);
+            } else {
+                mSearchView.setInputType(EditorInfo.TYPE_CLASS_PHONE);
+                mDialpadMenuItem.setIcon(R.drawable.baseline_keyboard_24);
+            }
+            return true;
+        } else if (itemId == R.id.menu_settings) {
+            ((HomeActivity) requireActivity()).goToSettings();
+            return true;
+        } else if (itemId == R.id.menu_about) {
+            ((HomeActivity) requireActivity()).goToAbout();
+            return true;
         }
+        return false;
     }
 
     @Override
@@ -228,7 +222,7 @@ public class SmartListFragment extends BaseSupportFragment<SmartListPresenter> i
     @Override
     public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
         binding = FragSmartlistBinding.inflate(inflater, container, false);
-        ((JamiApplication) getActivity().getApplication()).getInjectionComponent().inject(this);
+        ((JamiApplication) requireActivity().getApplication()).getInjectionComponent().inject(this);
         return binding.getRoot();
     }
 
@@ -244,15 +238,9 @@ public class SmartListFragment extends BaseSupportFragment<SmartListPresenter> i
         super.onViewCreated(view, savedInstanceState);
 
         binding.qrCode.setOnClickListener(v -> presenter.clickQRSearch());
-
         //binding.newGroup.setOnClickListener(v -> startNewGroup());
 
         binding.confsList.addOnScrollListener(new RecyclerView.OnScrollListener() {
-            @Override
-            public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
-                super.onScrollStateChanged(recyclerView, newState);
-            }
-
             @Override
             public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
                 boolean canScrollUp = recyclerView.canScrollVertically(SCROLL_DIRECTION_UP);
@@ -307,8 +295,8 @@ public class SmartListFragment extends BaseSupportFragment<SmartListPresenter> i
     }
 
     @Override
-    public void removeConversation(net.jami.model.Uri callContact) {
-        presenter.removeConversation(callContact);
+    public void removeConversation(net.jami.model.Uri conversationUri) {
+        presenter.removeConversation(conversationUri);
     }
 
     @Override
@@ -318,13 +306,9 @@ public class SmartListFragment extends BaseSupportFragment<SmartListPresenter> i
 
     @Override
     public void copyContactNumberToClipboard(String contactNumber) {
-        ClipboardHelper.copyNumberToClipboard(getActivity(), contactNumber, this);
-    }
-
-    @Override
-    public void clipBoardDidCopyNumber(String copiedNumber) {
+        ClipboardHelper.copyToClipboard(requireContext(), contactNumber);
         String snackbarText = getString(R.string.conversation_action_copied_peer_number_clipboard,
-                ActionHelper.getShortenedNumber(copiedNumber));
+                ActionHelper.getShortenedNumber(contactNumber));
         Snackbar.make(binding.listCoordinator, snackbarText, Snackbar.LENGTH_LONG).show();
     }
 
@@ -496,7 +480,7 @@ public class SmartListFragment extends BaseSupportFragment<SmartListPresenter> i
         if (binding == null || binding.separator == null)
             return;
 
-        if (DeviceUtils.isTablet(getContext())) {
+        if (DeviceUtils.isTablet(binding.getRoot().getContext())) {
             int margin = 0;
 
             if (open) {
@@ -514,21 +498,9 @@ public class SmartListFragment extends BaseSupportFragment<SmartListPresenter> i
         }
     }
 
-    private void selectFirstItem() {
-        if (mSmartListAdapter != null && mSmartListAdapter.getItemCount() > 0) {
-            new Handler().postDelayed(() -> {
-                if (binding != null) {
-                    RecyclerView.ViewHolder holder = binding.confsList.findViewHolderForAdapterPosition(0);
-                    if (holder != null)
-                        holder.itemView.performClick();
-                }
-
-            }, 100);
-        }
-    }
-
     private void setTabletQRLayout(boolean show) {
-        if (!DeviceUtils.isTablet(getContext()))
+        Context context = requireContext();
+        if (!DeviceUtils.isTablet(context))
             return;
 
         RelativeLayout.LayoutParams params =
@@ -539,8 +511,8 @@ public class SmartListFragment extends BaseSupportFragment<SmartListPresenter> i
         } else {
             params.removeRule(RelativeLayout.BELOW);
             TypedValue value = new TypedValue();
-            if (getActivity().getTheme().resolveAttribute(android.R.attr.actionBarSize, value, true)) {
-                params.topMargin = TypedValue.complexToDimensionPixelSize(value.data, getResources().getDisplayMetrics());
+            if (context.getTheme().resolveAttribute(android.R.attr.actionBarSize, value, true)) {
+                params.topMargin = TypedValue.complexToDimensionPixelSize(value.data, context.getResources().getDisplayMetrics());
             }
         }
         binding.listCoordinator.setLayoutParams(params);
diff --git a/ring-android/app/src/main/java/cx/ring/service/CallNotificationService.java b/ring-android/app/src/main/java/cx/ring/service/CallNotificationService.java
index a9d595ac1..f78049766 100644
--- a/ring-android/app/src/main/java/cx/ring/service/CallNotificationService.java
+++ b/ring-android/app/src/main/java/cx/ring/service/CallNotificationService.java
@@ -32,12 +32,13 @@ import androidx.annotation.Nullable;
 import javax.inject.Inject;
 
 import cx.ring.application.JamiApplication;
+import cx.ring.services.NotificationServiceImpl;
+
 import net.jami.services.NotificationService;
 
 public class CallNotificationService extends Service {
     public static final String ACTION_START = "START";
     public static final String ACTION_STOP = "STOP";
-    private static final int NOTIF_CALL_ID = 1001;
 
     @Inject
     NotificationService mNotificationService;
@@ -55,13 +56,14 @@ public class CallNotificationService extends Service {
             Notification notification = (Notification) mNotificationService.showCallNotification(intent.getIntExtra(NotificationService.KEY_NOTIFICATION_ID, -1));
             if (notification != null) {
                 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q)
-                    startForeground(NOTIF_CALL_ID, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_PHONE_CALL | ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION);
+                    startForeground(NotificationServiceImpl.NOTIF_CALL_ID, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_PHONE_CALL | ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION);
                 else
-                    startForeground(NOTIF_CALL_ID, notification);
+                    startForeground(NotificationServiceImpl.NOTIF_CALL_ID, notification);
             }
         } else if (ACTION_STOP.equals(intent.getAction())) {
             stopForeground(true);
             stopSelf();
+            mNotificationService.cancelCallNotification();
         }
         return START_NOT_STICKY;
     }
diff --git a/ring-android/app/src/main/java/cx/ring/service/DRingService.java b/ring-android/app/src/main/java/cx/ring/service/DRingService.java
index 32ff9859b..319115791 100644
--- a/ring-android/app/src/main/java/cx/ring/service/DRingService.java
+++ b/ring-android/app/src/main/java/cx/ring/service/DRingService.java
@@ -654,6 +654,7 @@ public class DRingService extends Service {
         long id = extras.getLong(KEY_TRANSFER_ID);
         ConversationPath path = ConversationPath.fromUri(uri);
         if (action.equals(ACTION_FILE_ACCEPT)) {
+            mNotificationService.removeTransferNotification(path.getAccountId(), path.getConversationUri(), id);
             mAccountService.acceptFileTransfer(path.getAccountId(), path.getConversationUri(), id);
         } else if (action.equals(ACTION_FILE_CANCEL)) {
             mConversationFacade.cancelFileTransfer(path.getAccountId(), path.getConversationUri(), id);
diff --git a/ring-android/app/src/main/java/cx/ring/services/DataTransferService.java b/ring-android/app/src/main/java/cx/ring/services/DataTransferService.java
index 5e3654060..f8974fe85 100644
--- a/ring-android/app/src/main/java/cx/ring/services/DataTransferService.java
+++ b/ring-android/app/src/main/java/cx/ring/services/DataTransferService.java
@@ -25,6 +25,7 @@ import android.content.Intent;
 import android.content.pm.ServiceInfo;
 import android.os.Build;
 import android.os.IBinder;
+import android.util.Log;
 
 import androidx.annotation.Nullable;
 import androidx.core.app.NotificationManagerCompat;
@@ -34,58 +35,72 @@ import javax.inject.Inject;
 import cx.ring.application.JamiApplication;
 
 import net.jami.services.NotificationService;
-import net.jami.utils.Log;
+
+import java.util.HashSet;
+import java.util.Set;
 
 public class DataTransferService extends Service {
     private final String TAG = DataTransferService.class.getSimpleName();
+    public static final String ACTION_START = "startTransfer";
+    public static final String ACTION_STOP = "stopTransfer";
+    private static final int NOTIF_FILE_SERVICE_ID = 1002;
 
     @Inject
     NotificationService mNotificationService;
     private NotificationManagerCompat notificationManager;
+    private boolean started = false;
 
-    private boolean isFirst = true;
-    private static final int NOTIF_FILE_SERVICE_ID = 1002;
-    private int serviceNotificationId;
+    private int serviceNotificationId = 0;
+    private final Set<Integer> serviceNotifications = new HashSet<>();
 
     @Override
     public int onStartCommand(Intent intent, int flags, int startId) {
-        super.onStartCommand(intent, flags, startId);
-
         int notificationId = intent.getIntExtra(NotificationService.KEY_NOTIFICATION_ID, -1);
-        Notification notification = (Notification) mNotificationService.getDataTransferNotification(notificationId);
-
-        if (notification == null) {
-            stopSelf();
-            return START_NOT_STICKY;
+        String action = intent.getAction();
+        if (ACTION_START.equals(action)) {
+            serviceNotifications.add(notificationId);
+            Notification notification = (Notification) mNotificationService.getDataTransferNotification(notificationId);
+            // Log.w(TAG, "Updating notification " + intent);
+            if (!started) {
+                Log.w(TAG, "starting transfer service " + intent);
+                serviceNotificationId = notificationId;
+                started = true;
+            }
+            if (notificationId == serviceNotificationId) {
+                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q)
+                    startForeground(NOTIF_FILE_SERVICE_ID, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC);
+                else
+                    startForeground(NOTIF_FILE_SERVICE_ID, notification);
+            } else {
+                notificationManager.notify(notificationId, notification);
+            }
         }
-
-        //if (isFirst) {
-        //    isFirst = false;
-            mNotificationService.cancelFileNotification(notificationId, true);
-            serviceNotificationId = notificationId;
-            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q)
-                startForeground(NOTIF_FILE_SERVICE_ID, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC);
-            else
-                startForeground(NOTIF_FILE_SERVICE_ID, notification);
-        //}
-
-        if (mNotificationService.getDataTransferNotification(serviceNotificationId) == null) {
-            mNotificationService.cancelFileNotification(notificationId, true);
-            serviceNotificationId = notificationId;
+        else if (ACTION_STOP.equals(action)) {
+            serviceNotifications.remove(notificationId);
+            if (notificationId == serviceNotificationId) {
+                // The service notification is removed. Migrate service to other notification or stop it
+                serviceNotificationId = serviceNotifications.isEmpty() ? 0 : serviceNotifications.iterator().next();
+                if (serviceNotificationId == 0) {
+                    Log.w(TAG, "stopping transfer service " + intent);
+                    stopForeground(true);
+                    stopSelf();
+                    started = false;
+                } else {
+                    // migrate notification to service
+                    notificationManager.cancel(serviceNotificationId);
+                    Notification notification = (Notification) mNotificationService.getDataTransferNotification(serviceNotificationId);
+                    notificationManager.notify(NOTIF_FILE_SERVICE_ID, notification);
+                }
+            } else {
+                notificationManager.cancel(notificationId);
+            }
         }
-
-        if (notificationId == serviceNotificationId)
-            notificationManager.notify(NOTIF_FILE_SERVICE_ID, notification);
-        else
-            notificationManager.notify(notificationId, notification);
-
-
         return START_NOT_STICKY;
     }
 
     @Override
     public void onCreate() {
-        Log.d(TAG, "OnCreate(), Service has been initialized");
+        Log.d(TAG, "OnCreate(), DataTransferService has been initialized");
         ((JamiApplication) getApplication()).getInjectionComponent().inject(this);
         notificationManager = NotificationManagerCompat.from(this);
         super.onCreate();
@@ -94,7 +109,7 @@ public class DataTransferService extends Service {
 
     @Override
     public void onDestroy() {
-        Log.d(TAG, "OnDestroy(), Service has been destroyed");
+        Log.d(TAG, "OnDestroy(), DataTransferService has been destroyed");
         super.onDestroy();
     }
 
diff --git a/ring-android/app/src/main/java/cx/ring/services/DeviceRuntimeServiceImpl.java b/ring-android/app/src/main/java/cx/ring/services/DeviceRuntimeServiceImpl.java
index f5adc8df1..5467f4b9d 100644
--- a/ring-android/app/src/main/java/cx/ring/services/DeviceRuntimeServiceImpl.java
+++ b/ring-android/app/src/main/java/cx/ring/services/DeviceRuntimeServiceImpl.java
@@ -28,8 +28,9 @@ import android.net.ConnectivityManager;
 import android.net.NetworkInfo;
 import android.os.Build;
 import android.provider.ContactsContract;
+import android.system.ErrnoException;
+import android.system.Os;
 import android.util.Log;
-import android.widget.Toast;
 
 import androidx.core.content.ContextCompat;
 
@@ -46,6 +47,7 @@ import cx.ring.utils.AndroidFileUtils;
 import cx.ring.utils.NetworkUtils;
 
 import net.jami.services.DeviceRuntimeService;
+import net.jami.utils.FileUtils;
 import net.jami.utils.StringUtils;
 
 public class DeviceRuntimeServiceImpl extends DeviceRuntimeService {
@@ -100,6 +102,11 @@ public class DeviceRuntimeServiceImpl extends DeviceRuntimeService {
         return AndroidFileUtils.getTempPath(mContext, conversationId, name);
     }
 
+    @Override
+    public File getConversationDir(String conversationId) {
+        return AndroidFileUtils.getConversationDir(mContext, conversationId);
+    }
+
     @Override
     public File getCacheDir() {
         return mContext.getCacheDir();
@@ -179,6 +186,17 @@ public class DeviceRuntimeServiceImpl extends DeviceRuntimeService {
         return null;
     }
 
+    @Override
+    public boolean hardLinkOrCopy(File source, File dest) {
+        try {
+            Os.link(source.getAbsolutePath(), dest.getAbsolutePath());
+        } catch (ErrnoException e) {
+            Log.w(TAG, "Can't create hardlink, copying instead: " + e.getMessage());
+            return FileUtils.copyFile(source, dest);
+        }
+        return true;
+    }
+
     private boolean checkPermission(String permission) {
         return ContextCompat.checkSelfPermission(mContext, permission) == PackageManager.PERMISSION_GRANTED;
     }
diff --git a/ring-android/app/src/main/java/cx/ring/services/NotificationServiceImpl.java b/ring-android/app/src/main/java/cx/ring/services/NotificationServiceImpl.java
index d7e19aef8..0a833c99b 100644
--- a/ring-android/app/src/main/java/cx/ring/services/NotificationServiceImpl.java
+++ b/ring-android/app/src/main/java/cx/ring/services/NotificationServiceImpl.java
@@ -118,7 +118,7 @@ public class NotificationServiceImpl implements NotificationService {
 
     private static final String NOTIF_CALL_GROUP = "calls";
 
-    private static final int NOTIF_CALL_ID = 1001;
+    public static final int NOTIF_CALL_ID = 1001;
 
     private final SparseArray<NotificationCompat.Builder> mNotificationBuilders = new SparseArray<>();
     @Inject
@@ -133,10 +133,11 @@ public class NotificationServiceImpl implements NotificationService {
     protected HistoryService mHistoryService;
     @Inject
     protected DeviceRuntimeService mDeviceRuntimeService;
+
     private NotificationManagerCompat notificationManager;
     private final Random random = new Random();
     private int avatarSize;
-    private final LinkedHashMap<Integer, Conference> currentCalls = new LinkedHashMap<>();
+    private final LinkedHashMap<String, Conference> currentCalls = new LinkedHashMap<>();
     private final ConcurrentHashMap<Integer, Notification> callNotifications = new ConcurrentHashMap<>();
     private final ConcurrentHashMap<Integer, Notification> dataTransferNotifications = new ConcurrentHashMap<>();
 
@@ -249,9 +250,6 @@ public class NotificationServiceImpl implements NotificationService {
         }
 
         Call call = conference.getParticipants().get(0);
-
-        notificationManager.cancel(NOTIF_CALL_ID);
-
         PendingIntent gotoIntent = PendingIntent.getService(mContext,
                 random.nextInt(),
                 new Intent(DRingService.ACTION_CALL_VIEW)
@@ -269,6 +267,8 @@ public class NotificationServiceImpl implements NotificationService {
                     .setSound(null)
                     .setVibrate(null)
                     .setColorized(true)
+                    .setUsesChronometer(true)
+                    .setWhen(conference.getTimestampStart())
                     .setColor(mContext.getResources().getColor(R.color.color_primary_light))
                     .addAction(R.drawable.baseline_call_end_24, mContext.getText(R.string.action_call_hangup),
                             PendingIntent.getService(mContext, random.nextInt(),
@@ -409,7 +409,7 @@ public class NotificationServiceImpl implements NotificationService {
         Notification notification = null;
 
         // Build notification
-        int id = conference.getId().hashCode();
+        String id = conference.getId();
         currentCalls.remove(id);
         if (!remove) {
             currentCalls.put(id, conference);
@@ -464,31 +464,37 @@ public class NotificationServiceImpl implements NotificationService {
      */
     @Override
     public void handleDataTransferNotification(DataTransfer transfer, Conversation conversation, boolean remove) {
-        Log.d(TAG, "handleDataTransferNotification, a data transfer event is in progress");
+        Log.d(TAG, "handleDataTransferNotification, a data transfer event is in progress " + remove);
         if (DeviceUtils.isTv(mContext)) {
             return;
         }
         if (!remove) {
             showFileTransferNotification(conversation, transfer);
         } else {
-            removeTransferNotification(transfer.getDaemonId());
+            removeTransferNotification(ConversationPath.toUri(conversation), transfer.getDaemonId());
         }
     }
 
+    @Override
+    public void removeTransferNotification(String accountId, Uri conversationUri, long transferId) {
+        removeTransferNotification(ConversationPath.toUri(accountId, conversationUri), transferId);
+    }
+
     /**
      * Cancels a data transfer notification and removes it from the list of notifications
      *
      * @param transferId the transfer id which is required to generate the notification id
      */
-    @Override
-    public void removeTransferNotification(long transferId) {
-        int id = getFileTransferNotificationId(transferId);
+    public void removeTransferNotification(android.net.Uri path, long transferId) {
+        int id = getFileTransferNotificationId(path, transferId);
         dataTransferNotifications.remove(id);
         cancelFileNotification(id, false);
-        if (dataTransferNotifications.isEmpty())
-            mContext.stopService(new Intent(mContext, DataTransferService.class));
-        else {
-            startForegroundService(dataTransferNotifications.keySet().iterator().next(), DataTransferService.class);
+        if (dataTransferNotifications.isEmpty()) {
+            mContext.startService(new Intent(DataTransferService.ACTION_STOP, path, mContext, DataTransferService.class)
+            .putExtra(KEY_NOTIFICATION_ID, id));
+        } else {
+            ContextCompat.startForegroundService(mContext, new Intent(DataTransferService.ACTION_STOP, path, mContext, DataTransferService.class)
+                    .putExtra(KEY_NOTIFICATION_ID, id));
         }
     }
 
@@ -505,11 +511,11 @@ public class NotificationServiceImpl implements NotificationService {
     public void showTextNotification(String accountId, Conversation conversation) {
         TreeMap<Long, TextMessage> texts = conversation.getUnreadTextMessages();
 
-        Log.w(TAG, "showTextNotification start " + accountId + " " + conversation.getUri() + " " + texts.size());
+        //Log.w(TAG, "showTextNotification start " + accountId + " " + conversation.getUri() + " " + texts.size());
 
         //TODO handle groups
         if (texts.isEmpty() || conversation.isVisible()) {
-            cancelTextNotification(conversation.getUri());
+            cancelTextNotification(conversation.getAccountId(), conversation.getUri());
             return;
         }
         if (texts.lastEntry().getValue().isNotified()) {
@@ -523,7 +529,8 @@ public class NotificationServiceImpl implements NotificationService {
     }
 
     private void textNotification(String accountId, TreeMap<Long, TextMessage> texts, Conversation conversation) {
-        android.net.Uri path = ConversationPath.toUri(conversation.getAccountId(), conversation.getUri());
+        ConversationPath cpath = new ConversationPath(conversation);
+        android.net.Uri path = cpath.toUri();
         Pair<Bitmap, String> conversationProfile = getProfile(conversation);
 
         int notificationVisibility = mPreferencesService.getSettings().getNotificationVisibility();
@@ -557,9 +564,9 @@ public class NotificationServiceImpl implements NotificationService {
                 .setAutoCancel(true)
                 .setColor(ResourcesCompat.getColor(mContext.getResources(), R.color.color_primary_dark, null));
 
-        String key = ConversationPath.toKey(accountId, conversation.getUri());
+        String key = cpath.toKey();
 
-        Person contactPerson = new Person.Builder()
+        Person conversationPerson = new Person.Builder()
                 .setKey(key)
                 .setName(conversationProfile.second)
                 .setIcon(conversationProfile.first == null ? null : IconCompat.createWithBitmap(conversationProfile.first))
@@ -600,7 +607,7 @@ public class NotificationServiceImpl implements NotificationService {
                 Contact contact = textMessage.getContact();
                 Bitmap contactPicture = getContactPicture(contact);
                 Person contactPerson = new Person.Builder()
-                        .setKey(textMessage.getAuthor())
+                        .setKey(ConversationPath.toKey(cpath.getAccountId(), contact.getUri().getUri()))
                         .setName(contact.getDisplayName())
                         .setIcon(contactPicture == null ? null : IconCompat.createWithBitmap(contactPicture))
                         .build();
@@ -613,7 +620,7 @@ public class NotificationServiceImpl implements NotificationService {
             messageNotificationBuilder.setStyle(history);
         }
 
-        int notificationId = getTextNotificationId(conversation.getUri());
+        int notificationId = getTextNotificationId(conversation.getAccountId(), conversation.getUri());
         int replyId = notificationId + 1;
         int markAsReadId = notificationId + 2;
 
@@ -677,8 +684,10 @@ public class NotificationServiceImpl implements NotificationService {
         Set<String> notifiedRequests = mPreferencesService.loadRequestsPreferences(account.getAccountID());
 
         Collection<Conversation> requests = account.getPending();
-        if (requests.isEmpty())
+        if (requests.isEmpty()) {
+            notificationManager.cancel(notificationId);
             return;
+        }
         if (requests.size() == 1) {
             Conversation request = requests.iterator().next();
             String contactKey = request.getUri().getRawUriString();
@@ -723,7 +732,7 @@ public class NotificationServiceImpl implements NotificationService {
             }
             if (!newRequest)
                 return;
-            builder.setContentText(String.format(mContext.getString(R.string.contact_request_msg), Integer.toString(requests.size())));
+            builder.setContentText(String.format(mContext.getString(R.string.contact_request_msg), requests.size()));
             builder.setLargeIcon(null);
             notificationManager.notify(notificationId, builder.build());
         }
@@ -738,17 +747,16 @@ public class NotificationServiceImpl implements NotificationService {
         if (event == null) {
             return;
         }
+        android.net.Uri path = ConversationPath.toUri(conversation);
+        Log.d(TAG, "showFileTransferNotification " + path);
         long dataTransferId = info.getDaemonId();
-        int notificationId = getFileTransferNotificationId(dataTransferId);
-
-        android.net.Uri path = ConversationPath.toUri(info.getAccount(), conversation.getUri());
+        int notificationId = getFileTransferNotificationId(path, dataTransferId);
 
         Intent intentConversation = new Intent(DRingService.ACTION_CONV_ACCEPT, path, mContext, DRingService.class);
 
         if (event.isOver()) {
-            removeTransferNotification(dataTransferId);
-
-            if (info.isOutgoing()) {
+            removeTransferNotification(path, dataTransferId);
+            if (info.isOutgoing() || info.isError()) {
                 return;
             }
 
@@ -816,7 +824,7 @@ public class NotificationServiceImpl implements NotificationService {
         if (event == Interaction.InteractionStatus.TRANSFER_CREATED) {
             messageNotificationBuilder.setDefaults(NotificationCompat.DEFAULT_VIBRATE);
             mNotificationBuilders.put(notificationId, messageNotificationBuilder);
-            updateNotification(messageNotificationBuilder.build(), notificationId);
+            // updateNotification(messageNotificationBuilder.build(), notificationId);
             return;
         } else {
             messageNotificationBuilder.setDefaults(NotificationCompat.DEFAULT_LIGHTS);
@@ -826,12 +834,12 @@ public class NotificationServiceImpl implements NotificationService {
             messageNotificationBuilder
                     .addAction(R.drawable.baseline_call_received_24, mContext.getText(R.string.accept),
                             PendingIntent.getService(mContext, random.nextInt(),
-                                    new Intent(DRingService.ACTION_FILE_ACCEPT, ConversationPath.toUri(conversation), mContext, DRingService.class)
+                                    new Intent(DRingService.ACTION_FILE_ACCEPT, path, mContext, DRingService.class)
                                             .putExtra(DRingService.KEY_TRANSFER_ID, dataTransferId),
                                     PendingIntent.FLAG_ONE_SHOT))
                     .addAction(R.drawable.baseline_cancel_24, mContext.getText(R.string.refuse),
                             PendingIntent.getService(mContext, random.nextInt(),
-                                    new Intent(DRingService.ACTION_FILE_CANCEL, ConversationPath.toUri(conversation), mContext, DRingService.class)
+                                    new Intent(DRingService.ACTION_FILE_CANCEL, path, mContext, DRingService.class)
                                             .putExtra(DRingService.KEY_TRANSFER_ID, dataTransferId),
                                     PendingIntent.FLAG_ONE_SHOT));
             mNotificationBuilders.put(notificationId, messageNotificationBuilder);
@@ -841,14 +849,15 @@ public class NotificationServiceImpl implements NotificationService {
             messageNotificationBuilder
                     .addAction(R.drawable.baseline_cancel_24, mContext.getText(android.R.string.cancel),
                             PendingIntent.getService(mContext, random.nextInt(),
-                                    new Intent(DRingService.ACTION_FILE_CANCEL)
-                                            .setClass(mContext, DRingService.class)
+                                    new Intent(DRingService.ACTION_FILE_CANCEL, path, mContext, DRingService.class)
                                             .putExtra(DRingService.KEY_TRANSFER_ID, dataTransferId),
                                     PendingIntent.FLAG_ONE_SHOT));
         }
         mNotificationBuilders.put(notificationId, messageNotificationBuilder);
         dataTransferNotifications.put(notificationId, messageNotificationBuilder.build());
-        startForegroundService(notificationId, DataTransferService.class);
+        ContextCompat.startForegroundService(mContext, new Intent(DataTransferService.ACTION_START, path, mContext, DataTransferService.class)
+                .putExtra(KEY_NOTIFICATION_ID, notificationId));
+        //startForegroundService(notificationId, DataTransferService.class);
     }
 
     @Override
@@ -898,17 +907,8 @@ public class NotificationServiceImpl implements NotificationService {
     }
 
     @Override
-    public void cancelTextNotification(Uri contact) {
-        if (contact == null) {
-            return;
-        }
-        int notificationId = getTextNotificationId(contact);
-        notificationManager.cancel(notificationId);
-        mNotificationBuilders.remove(notificationId);
-    }
-
     public void cancelTextNotification(String accountId, Uri contact) {
-        int notificationId = getTextNotificationId(contact);
+        int notificationId = getTextNotificationId(accountId, contact);
         notificationManager.cancel(notificationId);
         mNotificationBuilders.remove(notificationId);
     }
@@ -926,6 +926,7 @@ public class NotificationServiceImpl implements NotificationService {
     public void cancelCallNotification() {
         notificationManager.cancel(NOTIF_CALL_ID);
         mNotificationBuilders.remove(NOTIF_CALL_ID);
+        callNotifications.clear();
     }
 
     /**\
@@ -950,12 +951,12 @@ public class NotificationServiceImpl implements NotificationService {
         return (NOTIF_TRUST_REQUEST + accountId).hashCode();
     }
 
-    private int getTextNotificationId(Uri contact) {
-        return (NOTIF_MSG + contact.toString()).hashCode();
+    private int getTextNotificationId(String accountId, Uri contact) {
+        return (NOTIF_MSG + accountId + contact.toString()).hashCode();
     }
 
-    private int getFileTransferNotificationId(long dataTransferId) {
-        return (NOTIF_FILE_TRANSFER + dataTransferId).hashCode();
+    private int getFileTransferNotificationId(android.net.Uri path, long dataTransferId) {
+        return (NOTIF_FILE_TRANSFER + path.toString() + dataTransferId).hashCode();
     }
 
     private Bitmap getContactPicture(Contact contact) {
diff --git a/ring-android/app/src/main/java/cx/ring/services/SharedPreferencesServiceImpl.java b/ring-android/app/src/main/java/cx/ring/services/SharedPreferencesServiceImpl.java
index bc75a3806..355717553 100644
--- a/ring-android/app/src/main/java/cx/ring/services/SharedPreferencesServiceImpl.java
+++ b/ring-android/app/src/main/java/cx/ring/services/SharedPreferencesServiceImpl.java
@@ -39,6 +39,7 @@ import javax.inject.Inject;
 import cx.ring.R;
 import cx.ring.application.JamiApplication;
 import net.jami.model.Settings;
+import net.jami.model.Uri;
 import net.jami.services.PreferencesService;
 
 import cx.ring.utils.DeviceUtils;
@@ -194,6 +195,10 @@ public class SharedPreferencesServiceImpl extends PreferencesService {
                 .getInt(PREF_ACCEPT_IN_MAX_SIZE, 30) * 1024 * 1024;
 	}
 
+	public static SharedPreferences getConversationPreferences(@NonNull Context context, String accountId, Uri conversationUri) {
+        return context.getSharedPreferences(accountId + "_" + conversationUri.getUri(), Context.MODE_PRIVATE);
+    }
+
     private void applyDarkMode(boolean enabled) {
         AppCompatDelegate.setDefaultNightMode(
                 enabled ? AppCompatDelegate.MODE_NIGHT_YES
diff --git a/ring-android/app/src/main/java/cx/ring/services/VCardServiceImpl.java b/ring-android/app/src/main/java/cx/ring/services/VCardServiceImpl.java
index e074f6e56..ee957e584 100644
--- a/ring-android/app/src/main/java/cx/ring/services/VCardServiceImpl.java
+++ b/ring-android/app/src/main/java/cx/ring/services/VCardServiceImpl.java
@@ -38,6 +38,7 @@ import ezvcard.VCard;
 import ezvcard.parameter.ImageType;
 import ezvcard.property.Photo;
 import ezvcard.property.RawProperty;
+import io.reactivex.Maybe;
 import io.reactivex.Single;
 import io.reactivex.schedulers.Schedulers;
 
@@ -63,9 +64,9 @@ public class VCardServiceImpl extends VCardService {
     }
 
     @Override
-    public Single<VCard> loadSmallVCard(String accountId, int maxSize) {
+    public Maybe<VCard> loadSmallVCard(String accountId, int maxSize) {
         return VCardUtils.loadLocalProfileFromDisk(mContext.getFilesDir(), accountId)
-                .filter( vcard -> !VCardUtils.isEmpty(vcard)).toSingle()
+                .filter(vcard -> !VCardUtils.isEmpty(vcard))
                 .map(vcard -> {
                     if (!vcard.getPhotos().isEmpty()) {
                         // Reduce photo to fit in maxSize, assuming JPEG compress with ratio of at least 8
diff --git a/ring-android/app/src/main/java/cx/ring/settings/AccountFragment.java b/ring-android/app/src/main/java/cx/ring/settings/AccountFragment.java
index 2a7af1458..cb7f393d9 100644
--- a/ring-android/app/src/main/java/cx/ring/settings/AccountFragment.java
+++ b/ring-android/app/src/main/java/cx/ring/settings/AccountFragment.java
@@ -40,12 +40,11 @@ import cx.ring.account.JamiAccountSummaryFragment;
 import cx.ring.application.JamiApplication;
 import cx.ring.client.HomeActivity;
 import cx.ring.databinding.FragAccountBinding;
-import net.jami.model.Account;
+import io.reactivex.android.schedulers.AndroidSchedulers;
+import io.reactivex.disposables.CompositeDisposable;
+
 import net.jami.services.AccountService;
 
-/**
- * TODO: improvements : handle multiples permissions for feature.
- */
 public class AccountFragment extends Fragment implements ViewTreeObserver.OnScrollChangedListener {
 
     private static final int SCROLL_DIRECTION_UP = -1;
diff --git a/ring-android/app/src/main/java/cx/ring/share/ShareFragment.java b/ring-android/app/src/main/java/cx/ring/share/ShareFragment.java
index b59da7a03..8749cd56a 100644
--- a/ring-android/app/src/main/java/cx/ring/share/ShareFragment.java
+++ b/ring-android/app/src/main/java/cx/ring/share/ShareFragment.java
@@ -30,16 +30,17 @@ import android.view.ViewGroup;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
-import cx.ring.R;
-import cx.ring.application.JamiApplication;
-import cx.ring.databinding.FragShareBinding;
-import cx.ring.mvp.BaseSupportFragment;
 import net.jami.mvp.GenericView;
 import net.jami.share.SharePresenter;
 import net.jami.share.ShareViewModel;
 import net.jami.utils.QRCodeUtils;
 
-public class ShareFragment extends BaseSupportFragment<SharePresenter> implements GenericView<net.jami.share.ShareViewModel> {
+import cx.ring.R;
+import cx.ring.application.JamiApplication;
+import cx.ring.databinding.FragShareBinding;
+import cx.ring.mvp.BaseSupportFragment;
+
+public class ShareFragment extends BaseSupportFragment<SharePresenter> implements GenericView<ShareViewModel> {
 
     private String mUriToShow;
     private boolean isShareLocked = false;
diff --git a/ring-android/app/src/main/java/cx/ring/tv/call/TVCallFragment.java b/ring-android/app/src/main/java/cx/ring/tv/call/TVCallFragment.java
index aac77e416..a38fd4d13 100644
--- a/ring-android/app/src/main/java/cx/ring/tv/call/TVCallFragment.java
+++ b/ring-android/app/src/main/java/cx/ring/tv/call/TVCallFragment.java
@@ -86,6 +86,7 @@ import net.jami.model.Conference;
 import net.jami.model.Call;
 import cx.ring.mvp.BaseSupportFragment;
 
+import net.jami.model.Uri;
 import net.jami.services.DeviceRuntimeService;
 import net.jami.services.HardwareService;
 import cx.ring.tv.main.HomeActivity;
@@ -705,7 +706,7 @@ public class TVCallFragment extends BaseSupportFragment<CallPresenter> implement
     }
 
     @Override
-    public void goToConversation(String accountId, String conversationId) {
+    public void goToConversation(String accountId, Uri conversationId) {
 
     }
 
diff --git a/ring-android/app/src/main/java/cx/ring/tv/cards/contacts/ContactCard.java b/ring-android/app/src/main/java/cx/ring/tv/cards/contacts/ContactCard.java
index a1194a17b..af459b173 100644
--- a/ring-android/app/src/main/java/cx/ring/tv/cards/contacts/ContactCard.java
+++ b/ring-android/app/src/main/java/cx/ring/tv/cards/contacts/ContactCard.java
@@ -41,9 +41,9 @@ public class ContactCard extends Card {
     public void setModel(SmartListViewModel model) {
         mModel = model;
         setTitle(mModel.getContactName());
-        String username = mModel.getContact().get(0).getRingUsername();
+        String username = mModel.getContacts().get(0).getRingUsername();
         setDescription(username);
-        boolean isOnline = mModel.getContact().get(0).isOnline();
+        boolean isOnline = mModel.getContacts().get(0).isOnline();
         if (mModel.getContactName().equals(username)) {
             if (isOnline) {
                 setType(Type.CONTACT_ONLINE);
diff --git a/ring-android/app/src/main/java/cx/ring/tv/contact/TVContactPresenter.java b/ring-android/app/src/main/java/cx/ring/tv/contact/TVContactPresenter.java
index bd3a469e2..cfd0056e1 100644
--- a/ring-android/app/src/main/java/cx/ring/tv/contact/TVContactPresenter.java
+++ b/ring-android/app/src/main/java/cx/ring/tv/contact/TVContactPresenter.java
@@ -100,7 +100,7 @@ public class TVContactPresenter extends RootPresenter<TVContactView> {
 
     private void sendTrustRequest(String accountId, Uri conversationUri) {
         Conversation conversation = mAccountService.getAccount(accountId).getByUri(conversationUri);
-        mVCardService.loadSmallVCard(accountId, VCardService.MAX_SIZE_REQUEST)
+        mVCardService.loadSmallVCardWithDefault(accountId, VCardService.MAX_SIZE_REQUEST)
                 .subscribe(vCard -> mAccountService.sendTrustRequest(conversation, conversationUri, Blob.fromString(VCardUtils.vcardToString(vCard))),
                         e -> mAccountService.sendTrustRequest(conversation, conversationUri, null));
     }
diff --git a/ring-android/app/src/main/java/cx/ring/tv/contactrequest/TVContactRequestDetailPresenter.java b/ring-android/app/src/main/java/cx/ring/tv/contactrequest/TVContactRequestDetailPresenter.java
index c2c3c2a40..6157c803d 100644
--- a/ring-android/app/src/main/java/cx/ring/tv/contactrequest/TVContactRequestDetailPresenter.java
+++ b/ring-android/app/src/main/java/cx/ring/tv/contactrequest/TVContactRequestDetailPresenter.java
@@ -29,7 +29,7 @@ public class TVContactRequestDetailPresenter extends AbstractDetailsDescriptionP
     protected void onBindDescription(ViewHolder viewHolder, Object item) {
         SmartListViewModel viewModel = (SmartListViewModel) item;
         if (viewModel != null) {
-            String id = viewModel.getContact().get(0).getRingUsername();
+            String id = viewModel.getContacts().get(0).getRingUsername();
             String displayName = viewModel.getContactName();
             viewHolder.getTitle().setText(displayName);
             if (!displayName.equals(id))
diff --git a/ring-android/app/src/main/java/cx/ring/tv/conversation/TvConversationAdapter.java b/ring-android/app/src/main/java/cx/ring/tv/conversation/TvConversationAdapter.java
index 1fb83aa49..7fa1975dd 100644
--- a/ring-android/app/src/main/java/cx/ring/tv/conversation/TvConversationAdapter.java
+++ b/ring-android/app/src/main/java/cx/ring/tv/conversation/TvConversationAdapter.java
@@ -57,6 +57,17 @@ import com.bumptech.glide.load.resource.bitmap.CenterInside;
 import com.bumptech.glide.load.resource.bitmap.RoundedCorners;
 import com.bumptech.glide.request.target.DrawableImageViewTarget;
 
+import net.jami.conversation.ConversationPresenter;
+import net.jami.model.Call;
+import net.jami.model.Contact;
+import net.jami.model.ContactEvent;
+import net.jami.model.DataTransfer;
+import net.jami.model.Interaction;
+import net.jami.model.Interaction.InteractionStatus;
+import net.jami.model.Interaction.InteractionType;
+import net.jami.model.TextMessage;
+import net.jami.utils.StringUtils;
+
 import java.io.File;
 import java.text.DateFormat;
 import java.util.ArrayList;
@@ -67,22 +78,12 @@ import java.util.concurrent.TimeUnit;
 
 import cx.ring.R;
 import cx.ring.client.MediaViewerActivity;
-import net.jami.conversation.ConversationPresenter;
-import net.jami.model.Contact;
-import net.jami.model.ContactEvent;
-import net.jami.model.DataTransfer;
-import net.jami.model.Interaction;
-import net.jami.model.Interaction.InteractionStatus;
-import net.jami.model.Interaction.InteractionType;
-import net.jami.model.Call;
-import net.jami.model.TextMessage;
 import cx.ring.service.DRingService;
 import cx.ring.utils.AndroidFileUtils;
 import cx.ring.utils.ContentUriHandler;
 import cx.ring.utils.GlideApp;
 import cx.ring.utils.GlideOptions;
 import cx.ring.utils.ResourceMapper;
-import net.jami.utils.StringUtils;
 import io.reactivex.Observable;
 import io.reactivex.android.schedulers.AndroidSchedulers;
 import io.reactivex.schedulers.Schedulers;
@@ -103,9 +104,9 @@ public class TvConversationAdapter extends RecyclerView.Adapter<TvConversationVi
 
     private int expandedItemPosition = -1;
     private int lastDeliveredPosition = -1;
-    private Observable<Long> timestampUpdateTimer;
+    private final Observable<Long> timestampUpdateTimer;
 
-    private static int[] msgBGLayouts = new int[] {
+    private static final int[] msgBGLayouts = new int[] {
             R.drawable.textmsg_bg_out_last,
             R.drawable.textmsg_bg_out_middle,
             R.drawable.textmsg_bg_out_first,
@@ -216,7 +217,6 @@ public class TvConversationAdapter extends RecyclerView.Adapter<TvConversationVi
         }
 
         Interaction interaction = mInteractions.get(position);
-
         if (interaction != null) {
             switch (interaction.getType()) {
                 case CONTACT:
@@ -557,12 +557,9 @@ public class TvConversationAdapter extends RecyclerView.Adapter<TvConversationVi
         } else if (type == TransferMsgType.AUDIO) {
             Context context = viewHolder.itemView.getContext();
 
-            viewHolder.itemView.setOnFocusChangeListener(new View.OnFocusChangeListener() {
-                @Override
-                public void onFocusChange(View v, boolean hasFocus) {
-                    viewHolder.itemView.setBackgroundResource(hasFocus? R.drawable.tv_item_selected_background : R.drawable.tv_item_unselected_background);
-                    viewHolder.mAudioInfoLayout.animate().scaleY(hasFocus? 1.1f : 1f).scaleX(hasFocus? 1.1f : 1f);
-                }
+            viewHolder.itemView.setOnFocusChangeListener((v, hasFocus) -> {
+                viewHolder.itemView.setBackgroundResource(hasFocus? R.drawable.tv_item_selected_background : R.drawable.tv_item_unselected_background);
+                viewHolder.mAudioInfoLayout.animate().scaleY(hasFocus? 1.1f : 1f).scaleX(hasFocus? 1.1f : 1f);
             });
 
             try {
diff --git a/ring-android/app/src/main/java/cx/ring/tv/main/HomeActivity.java b/ring-android/app/src/main/java/cx/ring/tv/main/HomeActivity.java
index 6a469b257..e24566ffe 100644
--- a/ring-android/app/src/main/java/cx/ring/tv/main/HomeActivity.java
+++ b/ring-android/app/src/main/java/cx/ring/tv/main/HomeActivity.java
@@ -23,15 +23,16 @@ package cx.ring.tv.main;
 import android.content.Intent;
 import android.os.Bundle;
 
+import androidx.fragment.app.FragmentActivity;
 import androidx.leanback.app.BackgroundManager;
 import androidx.leanback.app.GuidedStepSupportFragment;
-import androidx.fragment.app.FragmentActivity;
+
+import net.jami.services.AccountService;
 
 import javax.inject.Inject;
 
 import cx.ring.R;
 import cx.ring.application.JamiApplication;
-import net.jami.services.AccountService;
 import cx.ring.tv.account.TVAccountWizard;
 import io.reactivex.android.schedulers.AndroidSchedulers;
 import io.reactivex.disposables.CompositeDisposable;
diff --git a/ring-android/app/src/main/java/cx/ring/tv/main/MainFragment.java b/ring-android/app/src/main/java/cx/ring/tv/main/MainFragment.java
index cf38bd204..94878c8b3 100644
--- a/ring-android/app/src/main/java/cx/ring/tv/main/MainFragment.java
+++ b/ring-android/app/src/main/java/cx/ring/tv/main/MainFragment.java
@@ -302,7 +302,7 @@ public class MainFragment extends BaseBrowseFragment<MainPresenter> implements M
                             .setChannelId(channelId)
                             .setType(TvContractCompat.PreviewPrograms.TYPE_CLIP)
                             .setTitle(vm.getContactName())
-                            .setAuthor(vm.getContact().get(0).getRingUsername())
+                            .setAuthor(vm.getContacts().get(0).getRingUsername())
                             .setPosterArtAspectRatio(TvContractCompat.PreviewPrograms.ASPECT_RATIO_1_1)
                             .setPosterArtUri(uri)
                             .setIntentUri(new Uri.Builder()
diff --git a/ring-android/app/src/main/java/cx/ring/utils/AndroidFileUtils.java b/ring-android/app/src/main/java/cx/ring/utils/AndroidFileUtils.java
index ade26eda7..f59739659 100644
--- a/ring-android/app/src/main/java/cx/ring/utils/AndroidFileUtils.java
+++ b/ring-android/app/src/main/java/cx/ring/utils/AndroidFileUtils.java
@@ -361,7 +361,7 @@ public class AndroidFileUtils {
         return context.getFileStreamPath(filename);
     }
 
-    public static File getConversationPath(Context context, String conversationId, String name) {
+    public static File getConversationDir(Context context, String conversationId) {
         File conversationsDir = getFilePath(context, "conversation_data");
 
         if (!conversationsDir.exists())
@@ -371,7 +371,11 @@ public class AndroidFileUtils {
         if (!conversationDir.exists())
             conversationDir.mkdir();
 
-        return new File(conversationDir, name);
+        return conversationDir;
+    }
+
+    public static File getConversationPath(Context context, String conversationId, String name) {
+        return new File(getConversationDir(context, conversationId), name);
     }
 
     public static File getTempPath(Context context, String conversationId, String name) {
diff --git a/ring-android/app/src/main/java/cx/ring/utils/BluetoothWrapper.java b/ring-android/app/src/main/java/cx/ring/utils/BluetoothWrapper.java
index 9f8be66bf..baf5db9bf 100644
--- a/ring-android/app/src/main/java/cx/ring/utils/BluetoothWrapper.java
+++ b/ring-android/app/src/main/java/cx/ring/utils/BluetoothWrapper.java
@@ -35,8 +35,7 @@ import java.util.List;
 import java.util.Set;
 
 public class BluetoothWrapper {
-
-    private static String TAG = BluetoothWrapper.class.getSimpleName();
+    private static final String TAG = BluetoothWrapper.class.getSimpleName();
     private static final boolean DBG = false;
 
     private final Context mContext;
diff --git a/ring-android/app/src/main/java/cx/ring/utils/ClipboardHelper.java b/ring-android/app/src/main/java/cx/ring/utils/ClipboardHelper.java
index 9c92591b0..73e8b90a4 100644
--- a/ring-android/app/src/main/java/cx/ring/utils/ClipboardHelper.java
+++ b/ring-android/app/src/main/java/cx/ring/utils/ClipboardHelper.java
@@ -19,44 +19,30 @@
  */
 package cx.ring.utils;
 
-import android.app.Activity;
 import android.content.ClipData;
 import android.content.ClipboardManager;
 import android.content.Context;
 import android.text.TextUtils;
 import android.util.Log;
 
+import androidx.annotation.NonNull;
+
 import cx.ring.BuildConfig;
+import cx.ring.R;
 
 public class ClipboardHelper {
     public static final String TAG = ClipboardHelper.class.getSimpleName();
-    public static final String COPY_CALL_CONTACT_NUMBER_CLIP_LABEL =
-            BuildConfig.APPLICATION_ID + ".clipboard.contactNumber";
 
-    public static void copyNumberToClipboard(final Activity activity,
-                                             final String number,
-                                             final ClipboardHelperCallback callback) {
-        if (TextUtils.isEmpty(number)) {
+    public static void copyToClipboard(final @NonNull Context context,
+                                       final String text) {
+        if (TextUtils.isEmpty(text)) {
             Log.d(TAG, "copyNumberToClipboard: number is null");
             return;
         }
 
-        if (activity == null) {
-            Log.d(TAG, "copyNumberToClipboard: activity is null");
-            return;
-        }
-
-        ClipboardManager clipboard = (ClipboardManager) activity
+        ClipboardManager clipboard = (ClipboardManager) context
                 .getSystemService(Context.CLIPBOARD_SERVICE);
-        ClipData clip = android.content.ClipData.newPlainText(COPY_CALL_CONTACT_NUMBER_CLIP_LABEL,
-                number);
+        ClipData clip = android.content.ClipData.newPlainText(context.getText(R.string.clip_contact_uri), text);
         clipboard.setPrimaryClip(clip);
-        if (callback != null) {
-            callback.clipBoardDidCopyNumber(number);
-        }
-    }
-
-    public interface ClipboardHelperCallback {
-        void clipBoardDidCopyNumber(String copiedNumber);
     }
 }
diff --git a/ring-android/app/src/main/java/cx/ring/utils/ConversationPath.java b/ring-android/app/src/main/java/cx/ring/utils/ConversationPath.java
index c8f695cf7..d3c2f9294 100644
--- a/ring-android/app/src/main/java/cx/ring/utils/ConversationPath.java
+++ b/ring-android/app/src/main/java/cx/ring/utils/ConversationPath.java
@@ -8,12 +8,14 @@ import android.text.TextUtils;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
+import java.util.Arrays;
 import java.util.List;
 import java.util.Objects;
 
 import cx.ring.fragments.ConversationFragment;
 import net.jami.model.Conversation;
 import net.jami.model.Interaction;
+import net.jami.utils.StringUtils;
 import net.jami.utils.Tuple;
 
 public class ConversationPath {
@@ -33,12 +35,20 @@ public class ConversationPath {
         conversationId = path.second;
     }
 
+    public ConversationPath(Conversation conversation) {
+        accountId = conversation.getAccountId();
+        conversationId = conversation.getUri().getUri();
+    }
+
     public String getAccountId() {
         return accountId;
     }
     public String getConversationId() {
         return conversationId;
     }
+    public net.jami.model.Uri getConversationUri() {
+        return net.jami.model.Uri.fromString(conversationId);
+    }
 
     @Deprecated
     public String getContactId() {
@@ -64,7 +74,10 @@ public class ConversationPath {
         return toUri(conversation.getAccountId(), conversation.getUri());
     }
     public static Uri toUri(@NonNull Interaction interaction) {
-        return toUri(interaction.getAccount(), net.jami.model.Uri.fromString(interaction.getConversation().getParticipant()));
+        if (interaction.getConversation() instanceof Conversation)
+            return toUri(interaction.getAccount(), ((Conversation) interaction.getConversation()).getUri());
+        else
+            return toUri(interaction.getAccount(), net.jami.model.Uri.fromString(interaction.getConversation().getParticipant()));
     }
 
     public Bundle toBundle() {
@@ -74,6 +87,7 @@ public class ConversationPath {
         bundle.putString(ConversationFragment.KEY_CONTACT_RING_ID, conversationId);
         bundle.putString(ConversationFragment.KEY_ACCOUNT_ID, accountId);
     }
+
     public static Bundle toBundle(String accountId, String uri) {
         Bundle bundle = new Bundle();
         bundle.putString(ConversationFragment.KEY_CONTACT_RING_ID, uri);
@@ -87,7 +101,9 @@ public class ConversationPath {
     public static String toKey(String accountId, String uri) {
         return TextUtils.join(",", Arrays.asList(accountId, uri));
     }
-
+    public String toKey() {
+        return toKey(accountId, conversationId);
+    }
     public static ConversationPath fromKey(String key) {
         if (key != null) {
             String[] keys = TextUtils.split(key, ",");
@@ -160,7 +176,4 @@ public class ConversationPath {
         return Objects.hash(accountId, conversationId);
     }
 
-    public net.jami.model.Uri getConversationUri() {
-        return net.jami.model.Uri.fromString(conversationId);
-    }
 }
diff --git a/ring-android/app/src/main/java/cx/ring/utils/ResourceMapper.java b/ring-android/app/src/main/java/cx/ring/utils/ResourceMapper.java
index 5096cc29d..3e340332b 100644
--- a/ring-android/app/src/main/java/cx/ring/utils/ResourceMapper.java
+++ b/ring-android/app/src/main/java/cx/ring/utils/ResourceMapper.java
@@ -28,33 +28,27 @@ import net.jami.model.Interaction.InteractionStatus;
 public class ResourceMapper {
 
     public static String getReadableFileTransferStatus(Context context, InteractionStatus transferStatus) {
-        if (transferStatus == InteractionStatus.TRANSFER_CREATED) {
-            return context.getString(R.string.file_transfer_status_created);
+        switch (transferStatus) {
+            case TRANSFER_CREATED:
+                return context.getString(R.string.file_transfer_status_created);
+            case TRANSFER_AWAITING_PEER:
+                return context.getString(R.string.file_transfer_status_wait_peer_acceptance);
+            case TRANSFER_AWAITING_HOST:
+                return context.getString(R.string.file_transfer_status_wait_host_acceptance);
+            case TRANSFER_ONGOING:
+                return context.getString(R.string.file_transfer_status_ongoing);
+            case TRANSFER_FINISHED:
+                return context.getString(R.string.file_transfer_status_finished);
+            case TRANSFER_CANCELED:
+                return context.getString(R.string.file_transfer_status_cancelled);
+            case TRANSFER_UNJOINABLE_PEER:
+                return context.getString(R.string.file_transfer_status_unjoinable_peer);
+            case TRANSFER_ERROR:
+                return context.getString(R.string.file_transfer_status_error);
+            case TRANSFER_TIMEOUT_EXPIRED:
+                return context.getString(R.string.file_transfer_status_timed_out);
+            default:
+                return "";
         }
-        if (transferStatus == InteractionStatus.TRANSFER_AWAITING_PEER) {
-            return context.getString(R.string.file_transfer_status_wait_peer_acceptance);
-        }
-        if (transferStatus == InteractionStatus.TRANSFER_AWAITING_HOST) {
-            return context.getString(R.string.file_transfer_status_wait_host_acceptance);
-        }
-        if (transferStatus == InteractionStatus.TRANSFER_ONGOING) {
-            return context.getString(R.string.file_transfer_status_ongoing);
-        }
-        if (transferStatus == InteractionStatus.TRANSFER_FINISHED) {
-            return context.getString(R.string.file_transfer_status_finished);
-        }
-        if (transferStatus == InteractionStatus.TRANSFER_CANCELED) {
-            return context.getString(R.string.file_transfer_status_cancelled);
-        }
-        if (transferStatus == InteractionStatus.TRANSFER_UNJOINABLE_PEER) {
-            return context.getString(R.string.file_transfer_status_unjoinable_peer);
-        }
-        if (transferStatus == InteractionStatus.TRANSFER_ERROR) {
-            return context.getString(R.string.file_transfer_status_error);
-        }
-        if (transferStatus == InteractionStatus.TRANSFER_TIMEOUT_EXPIRED) {
-            return context.getString(R.string.file_transfer_status_timed_out);
-        }
-        return "";
     }
 }
diff --git a/ring-android/app/src/main/java/cx/ring/views/AvatarDrawable.java b/ring-android/app/src/main/java/cx/ring/views/AvatarDrawable.java
index e2e87e923..38ea7c6b5 100644
--- a/ring-android/app/src/main/java/cx/ring/views/AvatarDrawable.java
+++ b/ring-android/app/src/main/java/cx/ring/views/AvatarDrawable.java
@@ -33,29 +33,33 @@ import android.graphics.RectF;
 import android.graphics.Shader;
 import android.graphics.Typeface;
 import android.graphics.drawable.Drawable;
+import android.graphics.drawable.VectorDrawable;
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.TypedValue;
 
 import androidx.annotation.NonNull;
 import androidx.core.content.ContextCompat;
-import cx.ring.R;
+
 import net.jami.model.Account;
 import net.jami.model.Contact;
 import net.jami.model.Conversation;
-import cx.ring.services.VCardServiceImpl;
 import net.jami.smartlist.SmartListViewModel;
-import cx.ring.utils.DeviceUtils;
 import net.jami.utils.HashUtils;
-import io.reactivex.Single;
-
-import android.graphics.drawable.VectorDrawable;
-import android.text.TextUtils;
-import android.util.TypedValue;
 
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
 
+import cx.ring.R;
+import cx.ring.services.VCardServiceImpl;
+import cx.ring.utils.DeviceUtils;
+import io.reactivex.Single;
+
 public class AvatarDrawable extends Drawable {
+    private static final String TAG = AvatarDrawable.class.getSimpleName();
+
     private static final int SIZE_AB = 36;
     private static final float SIZE_BORDER = 2f;
 
@@ -76,7 +80,11 @@ public class AvatarDrawable extends Drawable {
             R.color.brown_500, R.color.blue_grey_500
     };
 
-    private static class PresenceIndicatorInfo { int cx, cy, radius; };
+    private static class PresenceIndicatorInfo {
+        int cx, cy, radius;
+    }
+
+    ;
     private final PresenceIndicatorInfo presence = new PresenceIndicatorInfo();
 
     private boolean update = true;
@@ -101,6 +109,7 @@ public class AvatarDrawable extends Drawable {
     private final Paint presenceStrokePaint;
     private final Paint checkedPaint;
     private static final Paint drawPaint = new Paint();
+
     static {
         drawPaint.setAntiAlias(true);
         drawPaint.setFilterBitmap(true);
@@ -122,36 +131,44 @@ public class AvatarDrawable extends Drawable {
         private boolean isChecked = false;
         private boolean isGroup = false;
 
-        public Builder() {}
+        public Builder() {
+        }
 
         public Builder withId(String id) {
             this.id = id;
             return this;
         }
+
         public Builder withPhoto(Bitmap photo) {
             this.photos = photo == null ? null : Arrays.asList(photo); // list elements must be mutable
             return this;
         }
+
         public Builder withPhotos(List<Bitmap> photos) {
             this.photos = photos.isEmpty() ? null : photos;
             return this;
         }
+
         public Builder withName(String name) {
             this.name = name;
             return this;
         }
+
         public Builder withCircleCrop(boolean crop) {
             this.circleCrop = crop;
             return this;
         }
+
         public Builder withOnlineState(boolean isOnline) {
             this.isOnline = isOnline;
             return this;
         }
+
         public Builder withPresence(boolean showPresence) {
             this.showPresence = showPresence;
             return this;
         }
+
         public Builder withCheck(boolean checked) {
             this.isChecked = checked;
             return this;
@@ -160,10 +177,11 @@ public class AvatarDrawable extends Drawable {
         public Builder withNameData(String profileName, String username) {
             return withName(TextUtils.isEmpty(profileName) ? username : profileName);
         }
-        public Builder withContact(Contact contact){
+
+        public Builder withContact(Contact contact) {
             if (contact == null)
                 return this;
-            return withPhoto((Bitmap)contact.getPhoto())
+            return withPhoto((Bitmap) contact.getPhoto())
                     .withId(contact.getPrimaryNumber())
                     .withOnlineState(contact.isOnline())
                     .withNameData(contact.getProfileName(), contact.getUsername());
@@ -181,24 +199,24 @@ public class AvatarDrawable extends Drawable {
                     bitmaps.add(bitmap);
                 }
                 if (bitmaps.size() == 4)
-                    break;;
+                    break;
             }
-            if (bitmaps.isEmpty()) {
-                if (notTheUser == 1) {
-                    for (Contact contact : contacts) {
-                        if (!contact.isUser())
-                            return withContact(contact);
-                    }
-                } else if (notTheUser == 0) {
-                    // Fallback to the user avatar
-                    for (Contact contact : contacts)
+            if (notTheUser == 1) {
+                for (Contact contact : contacts) {
+                    if (!contact.isUser())
                         return withContact(contact);
                 }
-                return this;
+            }
+            if (bitmaps.isEmpty()) {
+                // Fallback to the user avatar
+                for (Contact contact : contacts)
+                    return withContact(contact);
             } else {
                 return withPhotos(bitmaps);
             }
+            return this;
         }
+
         public Builder withConversation(Conversation conversation) {
             return conversation.isSwarm()
                     ? withContacts(conversation.getContacts()).setGroup()
@@ -213,9 +231,10 @@ public class AvatarDrawable extends Drawable {
         public Builder withViewModel(SmartListViewModel vm) {
             boolean isSwarm = vm.getUri().isSwarm();
             return (isSwarm
-                    ? withContacts(vm.getContact()).setGroup()
-                    : withContact(vm.getContact().isEmpty() ? null : vm.getContact().get(vm.getContact().size() - 1)))
+                    ? withContacts(vm.getContacts()).setGroup()
+                    : withContact(vm.getContacts().isEmpty() ? null : vm.getContacts().get(vm.getContacts().size() - 1)))
                     .withPresence(vm.showPresence())
+                    .withOnlineState(vm.isOnline())
                     .withCheck(vm.isChecked());
         }
 
@@ -236,12 +255,13 @@ public class AvatarDrawable extends Drawable {
     public static Single<AvatarDrawable> load(Context context, Account account, boolean crop) {
         return VCardServiceImpl.loadProfile(account)
                 .map(data -> new Builder()
-                        .withPhoto((Bitmap)data.second)
+                        .withPhoto((Bitmap) data.second)
                         .withNameData(data.first, account.getRegisteredName())
                         .withId(account.getUri())
                         .withCircleCrop(crop)
                         .build(context));
     }
+
     public static Single<AvatarDrawable> load(Context context, Account account) {
         return load(context, account, true);
     }
@@ -252,19 +272,22 @@ public class AvatarDrawable extends Drawable {
         avatarText = convertNameToAvatarText(
                 TextUtils.isEmpty(profileName) ? username : profileName);
         if (bitmaps != null) {
-            bitmaps.set(0, (Bitmap)contact.getPhoto());
+            bitmaps.set(0, (Bitmap) contact.getPhoto());
         }
         isOnline = contact.isOnline();
         update = true;
     }
+
     public void setName(String name) {
         avatarText = convertNameToAvatarText(name);
         update = true;
     }
+
     public void setPhoto(Bitmap photo) {
         bitmaps.set(0, photo);
         update = true;
     }
+
     public void setOnline(boolean online) {
         isOnline = online;
     }
@@ -281,6 +304,9 @@ public class AvatarDrawable extends Drawable {
                 TypedValue.COMPLEX_UNIT_DIP, SIZE_AB, context.getResources().getDisplayMetrics());
         float borderSize = TypedValue.applyDimension(
                 TypedValue.COMPLEX_UNIT_DIP, SIZE_BORDER, context.getResources().getDisplayMetrics());
+        if (cropCircle) {
+            inSize = minSize;
+        }
         if (photos != null && photos.size() > 0) {
             avatarText = null;
             bitmaps = photos;
@@ -288,12 +314,12 @@ public class AvatarDrawable extends Drawable {
                 backgroundBounds = Collections.singletonList(new RectF());
                 inBounds = Collections.singletonList(null);
                 clipPaint = cropCircle ? Collections.singletonList(new Paint()) : null;
-                workspace = Arrays.asList((Bitmap)null);
+                workspace = Arrays.asList((Bitmap) null);
             } else {
                 backgroundBounds = new ArrayList<>(bitmaps.size());
                 inBounds = new ArrayList<>(bitmaps.size());
                 clipPaint = cropCircle ? new ArrayList<>(bitmaps.size()) : null;
-                workspace = cropCircle ? new ArrayList<>(bitmaps.size()) :  Arrays.asList((Bitmap)null);
+                workspace = cropCircle ? new ArrayList<>(bitmaps.size()) : Arrays.asList((Bitmap) null);
                 for (Bitmap ignored : bitmaps) {
                     backgroundBounds.add(new RectF());
                     inBounds.add(cropCircle ? null : new Rect());
@@ -308,7 +334,7 @@ public class AvatarDrawable extends Drawable {
                 }
             }
         } else {
-            workspace = Arrays.asList((Bitmap)null);
+            workspace = Arrays.asList((Bitmap) null);
             bitmaps = null;
             backgroundBounds = null;
             inBounds = null;
@@ -359,7 +385,7 @@ public class AvatarDrawable extends Drawable {
         bitmaps = other.bitmaps;
         backgroundBounds = other.backgroundBounds == null ? null : new ArrayList<>(other.backgroundBounds.size());
         if (backgroundBounds != null) {
-            for (int i=0, n=other.backgroundBounds.size(); i<n; i++) {
+            for (int i = 0, n = other.backgroundBounds.size(); i < n; i++) {
                 backgroundBounds.add(new RectF());
             }
         }
@@ -369,12 +395,12 @@ public class AvatarDrawable extends Drawable {
         placeholder = other.placeholder;
         avatarText = other.avatarText;
         workspace = new ArrayList<>(other.workspace.size());
-        for (int i=0, n=other.workspace.size(); i<n; i++) {
+        for (int i = 0, n = other.workspace.size(); i < n; i++) {
             workspace.add(null);
         }
         clipPaint = other.clipPaint == null ? null : new ArrayList<>(other.clipPaint.size());
         if (clipPaint != null) {
-            for (int i=0, n=other.clipPaint.size(); i<n; i++) {
+            for (int i = 0, n = other.clipPaint.size(); i < n; i++) {
                 clipPaint.add(new Paint(other.clipPaint.get(i)));
                 clipPaint.get(i).setShader(null);
             }
@@ -402,18 +428,20 @@ public class AvatarDrawable extends Drawable {
             update = false;
         }
         if (cropCircle) {
-            float r =  Math.min(getBounds().width(), getBounds().height()) / 2;
-            int cx = getBounds().centerX();
-            float cy = getBounds().height() / 2;
+            finalCanvas.save();
+            finalCanvas.translate((getBounds().width() - workspace.get(0).getWidth())  / 2.f, (getBounds().height() - workspace.get(0).getHeight())  / 2.f);
+            float r = Math.min(workspace.get(0).getWidth(), workspace.get(0).getHeight()) / 2;
+            int cx =  workspace.get(0).getWidth()/2;//getBounds().centerX();
+            float cy = workspace.get(0).getHeight()/2;//getBounds().height() / 2;
             int i = 0;
             final float ratio = 1.333333f;
             for (Paint paint : clipPaint) {
-                finalCanvas.drawCircle(cx, getBounds().bottom - cy, r, paint);
+                finalCanvas.drawCircle(cx, workspace.get(0).getHeight() - cy, r, paint);
                 if (i != 0) {
                     Shader s = paint.getShader();
                     paint.setShader(null);
                     paint.setStyle(Paint.Style.STROKE);
-                    finalCanvas.drawCircle(cx, getBounds().bottom - cy, r, paint);
+                    finalCanvas.drawCircle(cx, workspace.get(0).getHeight() - cy, r, paint);
                     paint.setShader(s);
                     paint.setStyle(Paint.Style.FILL);
                 }
@@ -421,6 +449,8 @@ public class AvatarDrawable extends Drawable {
                 r /= ratio;
                 cy /= ratio;
             }
+
+            finalCanvas.restore();
         } else {
             finalCanvas.drawBitmap(workspace.get(0), null, getBounds(), drawPaint);
         }
@@ -463,13 +493,14 @@ public class AvatarDrawable extends Drawable {
         presence.radius -= presenceStrokeWidth * 0.5;
 
         if (checkedIcon != null)
-            checkedIcon.setBounds(presence.cx - presence.radius, presence.cy - presence.radius, presence.cx + presence.radius,  presence.cy + presence.radius);
+            checkedIcon.setBounds(presence.cx - presence.radius, presence.cy - presence.radius, presence.cx + presence.radius, presence.cy + presence.radius);
     }
 
     private void drawPresence(@NonNull Canvas canvas) {
         canvas.drawCircle(presence.cx, presence.cy, presence.radius - 1, presenceFillPaint);
         canvas.drawCircle(presence.cx, presence.cy, presence.radius, presenceStrokePaint);
     }
+
     private void drawChecked(@NonNull Canvas canvas) {
         if (checkedIcon != null) {
             canvas.drawCircle(presence.cx, presence.cy, presence.radius, checkedPaint);
@@ -477,7 +508,7 @@ public class AvatarDrawable extends Drawable {
         }
     }
 
-    private static Rect getSubBounds(@NonNull Rect bounds, int total, int i)  {
+    private static Rect getSubBounds(@NonNull Rect bounds, int total, int i) {
         if (total == 1)
             return bounds;
 
@@ -505,7 +536,7 @@ public class AvatarDrawable extends Drawable {
         return null;
     }
 
-    private static <T> void fit(int iw, int ih, int bw, int bh, boolean outfit, T ret)  {
+    private static <T> void fit(int iw, int ih, int bw, int bh, boolean outfit, T ret) {
         int a = bw * ih;
         int b = bh * iw;
         int w;
@@ -520,9 +551,9 @@ public class AvatarDrawable extends Drawable {
         int x = (iw - w) / 2;
         int y = (ih - h) / 2;
         if (ret instanceof Rect)
-            ((Rect)ret).set(x, y, x + w, y + h);
+            ((Rect) ret).set(x, y, x + w, y + h);
         else if (ret instanceof RectF)
-            ((RectF)ret).set(x, y, x + w, y + h);
+            ((RectF) ret).set(x, y, x + w, y + h);
     }
 
     @Override
@@ -531,21 +562,20 @@ public class AvatarDrawable extends Drawable {
         setupPresenceIndicator(bounds);
         int d = Math.min(bounds.width(), bounds.height());
         if (placeholder != null) {
-            int cx = (bounds.width()-d)/2;
-            int cy = (bounds.height()-d)/2;
+            int cx = (bounds.width() - d) / 2;
+            int cy = (bounds.height() - d) / 2;
             placeholder.setBounds(cx, cy, cx + d, cy + d);
         }
         int iw = cropCircle ? d : bounds.width();
         int ih = cropCircle ? d : bounds.height();
-        for (int i=0, n=workspace.size(); i<n; i++) {
+        for (int i = 0, n = workspace.size(); i < n; i++) {
             if (workspace.get(i) != null) {
                 workspace.get(i).recycle();
                 workspace.set(i, null);
+                clipPaint.get(i).setShader(null);
             }
         }
         if (iw <= 0 || ih <= 0) {
-            for (Paint p : clipPaint)
-                p.setShader(null);
             return;
         }
         if (cropCircle) {
@@ -560,13 +590,13 @@ public class AvatarDrawable extends Drawable {
 
         if (bitmaps != null) {
             if (bitmaps.size() == 1 || cropCircle) {
-                for (int i=0; i<bitmaps.size(); i++) {
+                for (int i = 0; i < bitmaps.size(); i++) {
                     Bitmap bitmap = bitmaps.get(i);
                     fit(iw, ih, bitmap.getWidth(), bitmap.getHeight(), true, backgroundBounds.get(i));
                 }
             } else {
                 Rect realBounds = cropCircle ? new Rect(0, 0, iw, ih) : bounds;
-                for (int i=0; i<bitmaps.size(); i++) {
+                for (int i = 0; i < bitmaps.size(); i++) {
                     Bitmap bitmap = bitmaps.get(i);
                     Rect subBounds = getSubBounds(realBounds, bitmaps.size(), i);
                     if (subBounds != null) {
diff --git a/ring-android/app/src/main/res/layout/activity_contact_details.xml b/ring-android/app/src/main/res/layout/activity_contact_details.xml
index d85976f65..8f6c66278 100644
--- a/ring-android/app/src/main/res/layout/activity_contact_details.xml
+++ b/ring-android/app/src/main/res/layout/activity_contact_details.xml
@@ -57,10 +57,11 @@
                 app:titleTextColor="@color/colorOnPrimary" />
 
         </com.google.android.material.appbar.CollapsingToolbarLayout>
+
     </com.google.android.material.appbar.AppBarLayout>
 
     <com.google.android.material.floatingactionbutton.FloatingActionButton
-        android:id="@+id/fab"
+        android:id="@+id/sendMessage"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
         android:layout_margin="@dimen/fab_margin"
@@ -68,4 +69,17 @@
         app:layout_anchorGravity="bottom|end"
         app:srcCompat="@drawable/baseline_chat_24" />
 
+    <androidx.recyclerview.widget.RecyclerView
+        android:id="@+id/contactList"
+        android:background="@color/surface"
+        android:elevation="4dp"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_gravity="bottom"
+        android:orientation="horizontal"
+        android:visibility="gone"
+        app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
+        tools:listitem="@layout/item_contact_horizontal"
+        tools:visibility="visible"/>
+
 </androidx.coordinatorlayout.widget.CoordinatorLayout>
diff --git a/ring-android/app/src/main/res/layout/frag_conversation.xml b/ring-android/app/src/main/res/layout/frag_conversation.xml
index b680bcf78..291987c6d 100644
--- a/ring-android/app/src/main/res/layout/frag_conversation.xml
+++ b/ring-android/app/src/main/res/layout/frag_conversation.xml
@@ -52,7 +52,8 @@
                 android:layout_width="match_parent"
                 android:layout_height="wrap_content"
                 android:layout_below="@id/ongoingcall_pane"
-                android:orientation="vertical">
+                android:orientation="vertical"
+                android:visibility="gone">
 
                 <TextView
                     android:id="@+id/tvTrustRequestMessage"
diff --git a/ring-android/app/src/main/res/layout/item_contact_horizontal.xml b/ring-android/app/src/main/res/layout/item_contact_horizontal.xml
new file mode 100644
index 000000000..fa4d37a3b
--- /dev/null
+++ b/ring-android/app/src/main/res/layout/item_contact_horizontal.xml
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+Copyright (C) 2004-2016 Savoir-faire Linux Inc.
+
+Author: Alexandre Lision <alexandre.lision@savoirfairelinux.com>
+        Adrien Beraud <adrien.beraud@savoirfairelinux.com>
+        Romain Bertozzi <romain.bertozzi@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.
+-->
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="112dp"
+    android:layout_height="wrap_content"
+    android:background="?android:attr/selectableItemBackground"
+    android:descendantFocusability="blocksDescendants"
+    android:padding="8dp">
+
+    <ImageView
+        android:id="@+id/photo"
+        android:layout_width="60dp"
+        android:layout_height="60dp"
+        android:layout_centerHorizontal="true"
+        android:layout_margin="8dp"
+        android:contentDescription="@string/contact_picture_description"
+        android:scaleType="fitCenter"
+        tools:src="@drawable/baseline_person_24" />
+
+    <TextView
+        android:id="@+id/display_name"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_below="@+id/photo"
+        android:ellipsize="end"
+        android:gravity="center_horizontal"
+        android:layout_centerHorizontal="true"
+        android:maxLines="1"
+        android:singleLine="true"
+        android:textAlignment="center"
+        android:textColor="@color/textColorPrimary"
+        android:textIsSelectable="false"
+        android:textSize="16sp"
+        tools:text="Thomas" />
+
+</RelativeLayout>
diff --git a/ring-android/app/src/main/res/values/strings.xml b/ring-android/app/src/main/res/values/strings.xml
index 66a1fe34d..97cc1c45b 100644
--- a/ring-android/app/src/main/res/values/strings.xml
+++ b/ring-android/app/src/main/res/values/strings.xml
@@ -238,6 +238,7 @@ along with this program; if not, write to the Free Software
     <string name="block_contact_dialog_message">%1$s won\'t be able to contact you until you unblock it.</string>
     <string name="block_contact_completed">%1$s was blocked.</string>
     <string name="conversation_contact_is_typing">Contact is typing…</string>
+    <string name="conversation_info_contact_you">You</string>
 
     <string name="conversation_preference_color">Change conversation color</string>
     <string name="conversation_preference_emoji">Change conversation emoji</string>
diff --git a/ring-android/libringclient/src/main/java/net/jami/call/CallPresenter.java b/ring-android/libringclient/src/main/java/net/jami/call/CallPresenter.java
index 208bf9b16..6120ebd47 100644
--- a/ring-android/libringclient/src/main/java/net/jami/call/CallPresenter.java
+++ b/ring-android/libringclient/src/main/java/net/jami/call/CallPresenter.java
@@ -25,6 +25,7 @@ import net.jami.facades.ConversationFacade;
 import net.jami.model.Call;
 import net.jami.model.Conference;
 import net.jami.model.Conversation;
+import net.jami.model.ConversationHistory;
 import net.jami.model.Uri;
 import net.jami.mvp.RootPresenter;
 import net.jami.services.AccountService;
@@ -152,8 +153,8 @@ public class CallPresenter extends RootPresenter<CallView> {
                 }));*/
     }
 
-    public void initOutGoing(String accountId, Uri conversationUri, String contactId, boolean audioOnly) {
-        if (accountId == null || contactId == null) {
+    public void initOutGoing(String accountId, Uri conversationUri, String contactUri, boolean audioOnly) {
+        if (accountId == null || contactUri == null) {
             Log.e(TAG, "initOutGoing: null account or contact");
             hangupCall();
             return;
@@ -164,9 +165,9 @@ public class CallPresenter extends RootPresenter<CallView> {
         //getView().blockScreenRotation();
 
         Observable<Conference> callObservable = mCallService
-                .placeCall(accountId, conversationUri, Uri.fromString(StringUtils.toNumber(contactId)), audioOnly)
+                .placeCall(accountId, conversationUri, Uri.fromString(StringUtils.toNumber(contactUri)), audioOnly)
                 //.map(mCallService::getConference)
-                .flatMapObservable(call -> mCallService.getConfUpdates(call))
+                .flatMapObservable(mCallService::getConfUpdates)
                 .share();
 
         mCompositeDisposable.add(callObservable
@@ -176,7 +177,7 @@ public class CallPresenter extends RootPresenter<CallView> {
                     confUpdate(conference);
                 }, e -> {
                     hangupCall();
-                    net.jami.utils.Log.e(TAG, "Error with initOutgoing: " + e.getMessage());
+                    Log.e(TAG, "Error with initOutgoing: " + e.getMessage());
                 }));
 
         showConference(callObservable);
@@ -210,7 +211,7 @@ public class CallPresenter extends RootPresenter<CallView> {
                     }
                 }, e -> {
                     hangupCall();
-                    net.jami.utils.Log.e(TAG, "Error with initIncoming, preparing call flow :" , e);
+                    Log.e(TAG, "Error with initIncoming, preparing call flow :" , e);
                 }));
 
         // Handles retrieving call updates. Items emitted are only used if call is already in process or if user is returning to a call.
@@ -222,7 +223,7 @@ public class CallPresenter extends RootPresenter<CallView> {
                     }
                 }, e -> {
                     hangupCall();
-                    net.jami.utils.Log.e(TAG, "Error with initIncoming, action view flow: ", e);
+                    Log.e(TAG, "Error with initIncoming, action view flow: ", e);
                 }));
 
         showConference(callObservable);
@@ -260,13 +261,16 @@ public class CallPresenter extends RootPresenter<CallView> {
             return;
         }
         Call firstCall = mConference.getParticipants().get(0);
-        if (firstCall == null
-                || firstCall.getContact() == null
-                || firstCall.getContact().getIds() == null
-                || firstCall.getContact().getIds().isEmpty()) {
+        if (firstCall == null) {
             return;
         }
-        getView().goToConversation(firstCall.getAccount(), firstCall.getContact().getIds().get(0));
+        ConversationHistory c = firstCall.getConversation();
+        if (c instanceof Conversation) {
+            Conversation conversation = ((Conversation) c);
+            getView().goToConversation(conversation.getAccountId(), conversation.getUri());
+        } else if (firstCall.getContact() != null) {
+            getView().goToConversation(firstCall.getAccount(), firstCall.getContact().getConversationUri().blockingFirst());
+        }
     }
 
     public void speakerClick(boolean checked) {
@@ -427,7 +431,7 @@ public class CallPresenter extends RootPresenter<CallView> {
             // Updates of participant (and  pending participant) list
             Observable<List<Call>> callsObservable = mPendingSubject
                     .map(pendingList -> {
-                        net.jami.utils.Log.w(TAG, "mPendingSubject onNext " + pendingList.size() + " " + conference.getParticipants().size());
+                        Log.w(TAG, "mPendingSubject onNext " + pendingList.size() + " " + conference.getParticipants().size());
                         if (pendingList.isEmpty())
                             return conference.getParticipants();
                         List<Call> newList = new ArrayList<>(conference.getParticipants().size() + pendingList.size());
@@ -448,7 +452,7 @@ public class CallPresenter extends RootPresenter<CallView> {
             Observable<List<Call>> contactUpdates = contactsObservable
                     .switchMap(list -> Observable
                             .combineLatest(list, objects -> {
-                                net.jami.utils.Log.w(TAG, "flatMapObservable " + objects.length);
+                                Log.w(TAG, "flatMapObservable " + objects.length);
                                 ArrayList<Call> calls = new ArrayList<>(objects.length);
                                 for (Object call : objects)
                                     calls.add((Call)call);
@@ -458,7 +462,7 @@ public class CallPresenter extends RootPresenter<CallView> {
 
             contactDisposable = contactUpdates
                     .observeOn(mUiScheduler)
-                    .subscribe(cs -> getView().updateContactBubble(cs), e -> net.jami.utils.Log.e(TAG, "Error updating contact data", e));
+                    .subscribe(cs -> getView().updateContactBubble(cs), e -> Log.e(TAG, "Error updating contact data", e));
             mCompositeDisposable.add(contactDisposable);
         }
         mPendingSubject.onNext(mPendingCalls);
diff --git a/ring-android/libringclient/src/main/java/net/jami/call/CallView.java b/ring-android/libringclient/src/main/java/net/jami/call/CallView.java
index 21eca8e29..f82a97544 100644
--- a/ring-android/libringclient/src/main/java/net/jami/call/CallView.java
+++ b/ring-android/libringclient/src/main/java/net/jami/call/CallView.java
@@ -25,6 +25,7 @@ import java.util.Set;
 import net.jami.model.Call;
 import net.jami.model.Contact;
 import net.jami.model.Conference;
+import net.jami.model.Uri;
 import net.jami.services.HardwareService;
 
 public interface CallView {
@@ -62,7 +63,7 @@ public interface CallView {
     void resetPluginPreviewVideoSize(int previewWidth, int previewHeight, int rot);
     void resetVideoSize(int videoWidth, int videoHeight);
 
-    void goToConversation(String accountId, String conversationId);
+    void goToConversation(String accountId, Uri conversationId);
 
     void goToAddContact(Contact contact);
 
diff --git a/ring-android/libringclient/src/main/java/net/jami/conversation/ConversationPresenter.java b/ring-android/libringclient/src/main/java/net/jami/conversation/ConversationPresenter.java
index 260cf8f1f..9624e69f2 100644
--- a/ring-android/libringclient/src/main/java/net/jami/conversation/ConversationPresenter.java
+++ b/ring-android/libringclient/src/main/java/net/jami/conversation/ConversationPresenter.java
@@ -20,21 +20,16 @@
  */
 package net.jami.conversation;
 
-import java.io.File;
-
-import javax.inject.Inject;
-import javax.inject.Named;
-
 import net.jami.daemon.Blob;
 import net.jami.facades.ConversationFacade;
 import net.jami.model.Account;
-import net.jami.model.Contact;
+import net.jami.model.Call;
 import net.jami.model.Conference;
+import net.jami.model.Contact;
 import net.jami.model.Conversation;
 import net.jami.model.DataTransfer;
 import net.jami.model.Error;
 import net.jami.model.Interaction;
-import net.jami.model.Call;
 import net.jami.model.TrustRequest;
 import net.jami.model.Uri;
 import net.jami.mvp.RootPresenter;
@@ -48,6 +43,12 @@ import net.jami.utils.Log;
 import net.jami.utils.StringUtils;
 import net.jami.utils.Tuple;
 import net.jami.utils.VCardUtils;
+
+import java.io.File;
+
+import javax.inject.Inject;
+import javax.inject.Named;
+
 import io.reactivex.Observable;
 import io.reactivex.Scheduler;
 import io.reactivex.disposables.CompositeDisposable;
@@ -96,33 +97,22 @@ public class ConversationPresenter extends RootPresenter<ConversationView> {
         mCompositeDisposable.add(mVisibilityDisposable);
     }
 
-    /*@Override
-    public void bindView(ConversationView view) {
-        super.bindView(view);
-        mCompositeDisposable.add(mVisibilityDisposable);
-        if (mConversationDisposable == null && mConversation != null)
-            initView(mAccountService.getAccount(mConversation.getAccountId()), mConversation, view);
-    }*/
-
     public void init(Uri conversationUri, String accountId) {
         Log.w(TAG, "init " + conversationUri + " " + accountId);
         if (conversationUri.equals(mConversationUri))
             return;
         mConversationUri = conversationUri;
-        Account account = mAccountService.getAccount(accountId);
-        if (account != null) {
-            initContact(account, account.getByUri(mConversationUri), getView());
-            mCompositeDisposable.add(mConversationFacade.loadConversationHistory(account, conversationUri)
-                    .observeOn(mUiScheduler)
-                    .subscribe(c -> setConversation(account, c), e -> {
-                        Log.e(TAG, "Error loading conversation", e);
-                        getView().goToHome();
-                    }));
-        } else {
-            getView().goToHome();
-            return;
-        }
-
+        mCompositeDisposable.add(mConversationFacade.getAccountSubject(accountId)
+                .observeOn(mUiScheduler)
+                .flatMap(account -> mConversationFacade.loadConversationHistory(account, conversationUri)
+                            .map(c -> {
+                                setConversation(account, c);
+                                return c;
+                            }))
+                .subscribe(c -> {}, e -> {
+                    Log.e(TAG, "Error loading conversation", e);
+                    getView().goToHome();
+                }));
         getView().setReadIndicatorStatus(showReadIndicator());
     }
 
@@ -186,11 +176,11 @@ public class ConversationPresenter extends RootPresenter<ConversationView> {
         mConversationDisposable.clear();
         view.hideNumberSpinner();
 
-        if (account.isJami() && !c.isSwarm()) {
-            String accountId = account.getAccountID();
-            mConversationDisposable.add(c.getContact().getConversationUri()
+        if (account.isJami()) {
+            mConversationDisposable.add(c.getContact()
+                    .getConversationUri()
                     .observeOn(mUiScheduler)
-                    .subscribe(uri -> init(uri, accountId)));
+                    .subscribe(uri -> init(uri, account.getAccountID())));
         }
 
         mConversationDisposable.add(Observable.combineLatest(
@@ -277,7 +267,8 @@ public class ConversationPresenter extends RootPresenter<ConversationView> {
     }
 
     public void loadMore() {
-        mConversationFacade.loadMore(mConversation);
+        mConversationDisposable.add(mAccountService.loadMore(mConversation)
+                .subscribe(c -> {}, e-> {}));
     }
 
     public void openContact() {
@@ -354,12 +345,11 @@ public class ConversationPresenter extends RootPresenter<ConversationView> {
     }
 
     private void sendTrustRequest() {
-        //final Uri contactId = mConversationUri;
-        Contact contact = mConversation.getContact();//mAccountService.getAccount(accountId).getContactFromCache(contactId);
+        Contact contact = mConversation.getContact();
         if (contact != null) {
             contact.setStatus(Contact.Status.REQUEST_SENT);
         }
-        mVCardService.loadSmallVCard(mConversation.getAccountId(), VCardService.MAX_SIZE_REQUEST)
+        mVCardService.loadSmallVCardWithDefault(mConversation.getAccountId(), VCardService.MAX_SIZE_REQUEST)
                 .subscribeOn(Schedulers.computation())
                 .subscribe(vCard -> mAccountService.sendTrustRequest(mConversation, contact.getUri(), Blob.fromString(VCardUtils.vcardToString(vCard))),
                         e -> mAccountService.sendTrustRequest(mConversation, contact.getUri(), null));
@@ -462,7 +452,7 @@ public class ConversationPresenter extends RootPresenter<ConversationView> {
     }
 
     public void showPluginListHandlers() {
-        getView().showPluginListHandlers(mAccountId, mContactUri.getUri());
+        getView().showPluginListHandlers(mConversation.getAccountId(), mConversationUri.getUri());
     }
 
     public Tuple<String, String> getPath() {
diff --git a/ring-android/libringclient/src/main/java/net/jami/facades/ConversationFacade.java b/ring-android/libringclient/src/main/java/net/jami/facades/ConversationFacade.java
index da8d04ed7..ec34df577 100644
--- a/ring-android/libringclient/src/main/java/net/jami/facades/ConversationFacade.java
+++ b/ring-android/libringclient/src/main/java/net/jami/facades/ConversationFacade.java
@@ -38,7 +38,6 @@ import net.jami.services.HistoryService;
 import net.jami.services.NotificationService;
 import net.jami.services.PreferencesService;
 import net.jami.smartlist.SmartListViewModel;
-import net.jami.utils.FileUtils;
 import net.jami.utils.Log;
 import net.jami.utils.Tuple;
 
@@ -147,7 +146,7 @@ public class ConversationFacade {
                 })
                 .distinctUntilChanged()
                 .subscribe(t -> {
-                    net.jami.utils.Log.e(TAG, "Location reception started for " + t.second.contact);
+                    Log.e(TAG, "Location reception started for " + t.second.contact);
                     mNotificationService.showLocationNotification(t.first, t.second.contact);
                     mDisposableBag.add(t.second.location.doOnComplete(() ->
                             mNotificationService.cancelLocationNotification(t.first, t.second.contact)).subscribe());
@@ -155,11 +154,11 @@ public class ConversationFacade {
 
         mDisposableBag.add(mAccountService
                 .getMessageStateChanges()
-                .concatMapSingle(txt -> getAccountSubject(txt.getAccount())
-                        .map(a -> txt.getConversation() == null ? a.getSwarm(txt.getConversationId()) : a.getByUri(txt.getConversation().getParticipant()))
-                        .doOnSuccess(conversation -> conversation.updateTextMessage(txt)))
+                .concatMapSingle(e -> getAccountSubject(e.getAccount())
+                        .map(a -> e.getConversation() == null ? a.getSwarm(e.getConversationId()) : a.getByUri(e.getConversation().getParticipant()))
+                        .doOnSuccess(conversation -> conversation.updateInteraction(e)))
                 .subscribe(c -> {
-                }, e -> net.jami.utils.Log.e(TAG, "Error updating text message", e)));
+                }, e -> Log.e(TAG, "Error updating text message", e)));
 
         mDisposableBag.add(mAccountService
                 .getDataTransfers()
@@ -203,7 +202,7 @@ public class ConversationFacade {
             if (lastMessage != null) {
                 account.refreshed(conversation);
                 if (mPreferencesService.getSettings().isAllowReadIndicator()) {
-                    mAccountService.setMessageDisplayed(account.getAccountID(), conversation.getUri().getRawRingId(), lastMessage);
+                    mAccountService.setMessageDisplayed(account.getAccountID(), conversation.getUri(), lastMessage);
                 }
                 if (cancelNotification) {
                     mNotificationService.cancelTextNotification(account.getAccountID(), conversation.getUri());
@@ -264,13 +263,13 @@ public class ConversationFacade {
     }
 
     public void setIsComposing(String accountId, Uri conversationUri, boolean isComposing) {
-        mCallService.setIsComposing(accountId, conversationUri.getRawRingId(), isComposing);
+        mCallService.setIsComposing(accountId, conversationUri.getUri(), isComposing);
     }
 
     public Completable sendFile(Conversation conversation, Uri to, File file) {
         return Single.fromCallable(() -> {
             if (file == null || !file.exists() || !file.canRead()) {
-                net.jami.utils.Log.w(TAG, "sendFile: file not found or not readable: " + file);
+                Log.w(TAG, "sendFile: file not found or not readable: " + file);
                 return null;
             }
 
@@ -281,20 +280,19 @@ public class ConversationFacade {
                 mHistoryService.insertInteraction(conversation.getAccountId(), conversation, transfer).blockingAwait();
             }
 
-            File dest = mDeviceRuntimeService.getConversationPath(conversation.getUri().getRawRingId(), transfer.getStoragePath());
-            if (!FileUtils.moveFile(file, dest)) {
-                net.jami.utils.Log.e(TAG, "sendFile: can't move file to " + dest);
-                return null;
-            }
-
-            transfer.destination = dest;
+            transfer.destination = mDeviceRuntimeService.getConversationDir(conversation.getUri().getRawRingId());
             return transfer;
         })
-                .subscribeOn(Schedulers.io())
-                .flatMapCompletable(mAccountService::sendFile);
+                .flatMap(t -> mAccountService.sendFile(file, t))
+                .flatMapCompletable(transfer -> Completable.fromAction(() -> {
+                    File destination = new File(transfer.destination, transfer.getStoragePath());
+                    if (!mDeviceRuntimeService.hardLinkOrCopy(file, destination)) {
+                        Log.e(TAG, "sendFile: can't move file to " + destination);
+                    }
+                }))
+                .subscribeOn(Schedulers.io());
     }
 
-
     public void deleteConversationItem(Conversation conversation, Interaction element) {
         if (element.getType() == Interaction.InteractionType.DATA_TRANSFER) {
             DataTransfer transfer = (DataTransfer) element;
@@ -307,14 +305,13 @@ public class ConversationFacade {
                         Completable.fromAction(file::delete)
                                 .subscribeOn(Schedulers.io()))
                         .subscribe(() -> conversation.removeInteraction(transfer),
-                                e -> net.jami.utils.Log.e(TAG, "Can't delete file transfer", e)));
+                                e -> Log.e(TAG, "Can't delete file transfer", e)));
             }
         } else {
             // handling is the same for calls and texts
-            mDisposableBag.add(Completable.mergeArrayDelayError(mHistoryService.deleteInteraction(element.getId(), element.getAccount()).subscribeOn(Schedulers.io()))
-                    .andThen(startConversation(element.getAccount(), Uri.fromString(element.getConversation().getParticipant())))
-                    .subscribe(c -> c.removeInteraction(element),
-                            e -> net.jami.utils.Log.e(TAG, "Can't delete message", e)));
+            mDisposableBag.add(mHistoryService.deleteInteraction(element.getId(), element.getAccount()).subscribeOn(Schedulers.io())
+                    .subscribe(() -> conversation.removeInteraction(element),
+                            e -> Log.e(TAG, "Can't delete message", e)));
         }
     }
 
@@ -323,7 +320,7 @@ public class ConversationFacade {
                 mCallService.cancelMessage(message.getAccount(), message.getId()).subscribeOn(Schedulers.io()))
                 .andThen(startConversation(message.getAccount(), Uri.fromString(message.getConversation().getParticipant())))
                 .subscribe(c -> c.removeInteraction(message),
-                        e -> net.jami.utils.Log.e(TAG, "Can't cancel message sending", e)));
+                        e -> Log.e(TAG, "Can't cancel message sending", e)));
     }
 
     /**
@@ -335,7 +332,7 @@ public class ConversationFacade {
     private Single<Account> loadSmartlist(final Account account) {
         synchronized (account) {
             if (account.historyLoader == null) {
-                net.jami.utils.Log.d(TAG, "loadSmartlist(): start loading");
+                Log.d(TAG, "loadSmartlist(): start loading");
                 account.historyLoader = getSmartlist(account);
             }
             return account.historyLoader;
@@ -354,31 +351,25 @@ public class ConversationFacade {
         if (conversation == null)
             return Single.error(new RuntimeException("Can't get conversation"));
         synchronized (conversation) {
-            if (conversation.isSwarm()) {
-                loadMore(conversation);
-                Single<Conversation> ret = Single.just(conversation);
-                conversation.setLoaded(ret);
-                return ret;
-            }
-            if (conversation.getId() == null) {
+            if (!conversation.isSwarm() && conversation.getId() == null) {
                 return Single.just(conversation);
             }
             Single<Conversation> ret = conversation.getLoaded();
             if (ret == null) {
-                ret = getConversationHistory(conversation);
+                ret = conversation.isSwarm() ? mAccountService.loadMore(conversation) : getConversationHistory(conversation);
                 conversation.setLoaded(ret);
             }
             return ret;
         }
     }
 
-    private Observable<net.jami.smartlist.SmartListViewModel> observeConversation(Account account, Conversation conversation, boolean hasPresence) {
+    private Observable<SmartListViewModel> observeConversation(Account account, Conversation conversation, boolean hasPresence) {
         return Observable.merge(account.getConversationSubject()
                         .filter(c -> c == conversation)
                         .startWith(conversation),
                 mContactService
                         .observeContact(conversation.getAccountId(), conversation.getContacts(), hasPresence))
-                .map(e -> new net.jami.smartlist.SmartListViewModel(conversation, hasPresence));
+                .map(e -> new SmartListViewModel(conversation, hasPresence));
         /*return account.getConversationSubject()
                 .filter(c -> c == conversation)
                 .startWith(conversation)
@@ -387,49 +378,49 @@ public class ConversationFacade {
                         .map(contact -> new SmartListViewModel(c, hasPresence)));*/
     }
 
-    public Observable<List<Observable<net.jami.smartlist.SmartListViewModel>>> getSmartList(Observable<Account> currentAccount, boolean hasPresence) {
+    public Observable<List<Observable<SmartListViewModel>>> getSmartList(Observable<Account> currentAccount, boolean hasPresence) {
         return currentAccount.switchMap(account -> account.getConversationsSubject()
                 .switchMapSingle(conversations -> Observable.fromIterable(conversations)
                         .map(conversation -> observeConversation(account, conversation, hasPresence))
                         .toList()));
     }
 
-    public Observable<List<net.jami.smartlist.SmartListViewModel>> getContactList(Observable<Account> currentAccount) {
+    public Observable<List<SmartListViewModel>> getContactList(Observable<Account> currentAccount) {
         return currentAccount.switchMap(account -> account.getConversationsSubject()
                 .switchMapSingle(conversations -> Observable.fromIterable(conversations)
                         .filter(conversation -> !conversation.isSwarm())
-                        .map(conversation -> new net.jami.smartlist.SmartListViewModel(conversation, false))
+                        .map(conversation -> new SmartListViewModel(conversation, false))
                         .toList()));
     }
 
-    public Observable<List<Observable<net.jami.smartlist.SmartListViewModel>>> getPendingList(Observable<Account> currentAccount) {
+    public Observable<List<Observable<SmartListViewModel>>> getPendingList(Observable<Account> currentAccount) {
         return currentAccount.switchMap(account -> account.getPendingSubject()
                 .switchMapSingle(conversations -> Observable.fromIterable(conversations)
                         .map(conversation -> observeConversation(account, conversation, false))
                         .toList()));
     }
 
-    public Observable<List<Observable<net.jami.smartlist.SmartListViewModel>>> getSmartList(boolean hasPresence) {
+    public Observable<List<Observable<SmartListViewModel>>> getSmartList(boolean hasPresence) {
         return getSmartList(mAccountService.getCurrentAccountSubject(), hasPresence);
     }
 
-    public Observable<List<Observable<net.jami.smartlist.SmartListViewModel>>> getPendingList() {
+    public Observable<List<Observable<SmartListViewModel>>> getPendingList() {
         return getPendingList(mAccountService.getCurrentAccountSubject());
     }
 
-    public Observable<List<net.jami.smartlist.SmartListViewModel>> getContactList() {
+    public Observable<List<SmartListViewModel>> getContactList() {
         return getContactList(mAccountService.getCurrentAccountSubject());
     }
 
-    private Single<List<Observable<net.jami.smartlist.SmartListViewModel>>> getSearchResults(Account account, String query) {
+    private Single<List<Observable<SmartListViewModel>>> getSearchResults(Account account, String query) {
         Uri uri = Uri.fromString(query);
         if (account.isSip()) {
             Contact contact = account.getContactFromCache(uri);
             return mContactService.loadContactData(contact, account.getAccountID())
-                    .andThen(Single.just(Collections.singletonList(Observable.just(new net.jami.smartlist.SmartListViewModel(account.getAccountID(), contact, contact.getPrimaryNumber(), null)))));
+                    .andThen(Single.just(Collections.singletonList(Observable.just(new SmartListViewModel(account.getAccountID(), contact, contact.getPrimaryNumber(), null)))));
         } else if (uri.isHexId()) {
             return mContactService.getLoadedContact(account.getAccountID(), account.getContactFromCache(uri))
-                    .map(contact -> Collections.singletonList(Observable.just(new net.jami.smartlist.SmartListViewModel(account.getAccountID(), contact, contact.getPrimaryNumber(), null))));
+                    .map(contact -> Collections.singletonList(Observable.just(new SmartListViewModel(account.getAccountID(), contact, contact.getPrimaryNumber(), null))));
         } else if (account.canSearch() && !query.contains("@")) {
             return mAccountService.searchUser(account.getAccountID(), query)
                     .map(AccountService.UserSearchResult::getResultsViewModels);
@@ -439,22 +430,22 @@ public class ConversationFacade {
         }
     }
 
-    private Observable<List<Observable<net.jami.smartlist.SmartListViewModel>>> getSearchResults(Account account, Observable<String> query) {
+    private Observable<List<Observable<SmartListViewModel>>> getSearchResults(Account account, Observable<String> query) {
         return query.switchMapSingle(q -> q.isEmpty()
-                ? net.jami.smartlist.SmartListViewModel.EMPTY_LIST
+                ? SmartListViewModel.EMPTY_LIST
                 : getSearchResults(account, q))
                 .distinctUntilChanged();
     }
 
-    public Observable<List<Observable<net.jami.smartlist.SmartListViewModel>>> getFullList(Observable<Account> currentAccount, Observable<String> query, boolean hasPresence) {
+    public Observable<List<Observable<SmartListViewModel>>> getFullList(Observable<Account> currentAccount, Observable<String> query, boolean hasPresence) {
         return currentAccount.switchMap(account -> Observable.combineLatest(
                 account.getConversationsSubject(),
                 getSearchResults(account, query),
                 query,
                 (conversations, searchResults, q) -> {
-                    List<Observable<net.jami.smartlist.SmartListViewModel>> newList = new ArrayList<>(conversations.size() + searchResults.size() + 2);
+                    List<Observable<SmartListViewModel>> newList = new ArrayList<>(conversations.size() + searchResults.size() + 2);
                     if (!searchResults.isEmpty()) {
-                        newList.add(net.jami.smartlist.SmartListViewModel.TITLE_PUBLIC_DIR);
+                        newList.add(SmartListViewModel.TITLE_PUBLIC_DIR);
                         newList.addAll(searchResults);
                     }
                     if (!conversations.isEmpty()) {
@@ -485,8 +476,13 @@ public class ConversationFacade {
      * @param account the user account
      */
     private Single<Account> getSmartlist(final Account account) {
-        return mHistoryService.getSmartlist(account.getAccountID())
-                .map(conversationHistoryList -> {
+        List<Completable> actions = new ArrayList<>(account.getConversations().size() + 1);
+        for (Conversation c : account.getConversations())  {
+            if (c.isSwarm())
+                actions.add(c.getLastElementLoaded());
+        }
+        actions.add(mHistoryService.getSmartlist(account.getAccountID())
+                .flatMapCompletable(conversationHistoryList -> Completable.fromAction(() -> {
                     List<Conversation> conversations = new ArrayList<>();
                     for (Interaction e : conversationHistoryList) {
                         Conversation conversation = account.getByUri(e.getConversation().getParticipant());
@@ -497,8 +493,9 @@ public class ConversationFacade {
                         conversations.add(conversation);
                     }
                     account.setHistoryLoaded(conversations);
-                    return account;
-                })
+                })));
+        return Completable.merge(actions)
+                .andThen(Single.just(account))
                 .cache();
     }
 
@@ -547,7 +544,7 @@ public class ConversationFacade {
     }
 
     public void updateTextNotifications(String accountId, List<Conversation> conversations) {
-        net.jami.utils.Log.d(TAG, "updateTextNotifications() " + accountId + " " + conversations.size());
+        Log.d(TAG, "updateTextNotifications() " + accountId + " " + conversations.size());
 
         for (Conversation conversation : conversations) {
             mNotificationService.showTextNotification(accountId, conversation);
@@ -561,9 +558,9 @@ public class ConversationFacade {
             }
             if (mPreferencesService.getSettings().isAllowReadIndicator()) {
                 if (txt.getMessageId() != null) {
-                    mAccountService.setMessageDisplayed(txt.getAccount(), txt.getConversationId(), txt.getMessageId());
+                    mAccountService.setMessageDisplayed(txt.getAccount(), new Uri(Uri.SWARM_SCHEME, txt.getConversationId()), txt.getMessageId());
                 } else {
-                    mAccountService.setMessageDisplayed(txt.getAccount(), txt.getAuthor(), Long.toString(txt.getDaemonId(), 16));
+                    mAccountService.setMessageDisplayed(txt.getAccount(), new Uri(Uri.JAMI_URI_SCHEME, txt.getAuthor()), txt.getDaemonIdString());
                 }
             }
         }
@@ -592,6 +589,7 @@ public class ConversationFacade {
         if (transfer.getStatus() == Interaction.InteractionStatus.TRANSFER_CREATED && !transfer.isOutgoing()) {
             if (transfer.canAutoAccept(mPreferencesService.getMaxFileAutoAccept(transfer.getAccount()))) {
                 mAccountService.acceptFileTransfer(conversation, transfer);
+                return;
             }
         }
         mNotificationService.handleDataTransferNotification(transfer, conversation, conversation.isVisible());
@@ -614,13 +612,9 @@ public class ConversationFacade {
         String conversationId = call.getConversationId();
         Log.w(TAG, "CallStateChange " + call.getId() + " conversationId:" + conversationId);
 
-        Conversation conversation = account == null
-                ? null
-                : (conversationId == null
-                ? (contact == null
-                ? null
-                : account.getByUri(contact.getUri()))
-                : account.getSwarm(conversationId));
+        Conversation conversation = conversationId == null
+                ? (contact == null ? null : account.getByUri(contact.getUri()))
+                : account.getSwarm(conversationId);
         Conference conference = null;
         if (conversation != null) {
             conference = conversation.getConference(call.getDaemonIdString());
@@ -686,36 +680,48 @@ public class ConversationFacade {
 
     public void cancelFileTransfer(String accountId, Uri conversationId, long id) {
         mAccountService.cancelDataTransfer(accountId, conversationId.isSwarm() ? conversationId.getRawRingId() : "", id);
-        mNotificationService.removeTransferNotification(id);
+        mNotificationService.removeTransferNotification(accountId, conversationId, id);
         DataTransfer transfer = mAccountService.getAccount(accountId).getDataTransfer(id);
         if (transfer != null)
             deleteConversationItem((Conversation) transfer.getConversation(), transfer);
     }
 
-    public Completable removeConversation(String accountId, Uri contact) {
-        return mHistoryService
-                .clearHistory(contact.getUri(), accountId, true)
-                .doOnSubscribe(s -> {
-                    Account account = mAccountService.getAccount(accountId);
-                    account.clearHistory(contact, true);
-                    mAccountService.removeContact(accountId, contact.getRawRingId(), false);
-                });
+    public Completable removeConversation(String accountId, Uri conversationUri) {
+        if (conversationUri.isSwarm()) {
+            return mAccountService.removeConversation(accountId, conversationUri);
+        } else {
+            return mHistoryService
+                    .clearHistory(conversationUri.getUri(), accountId, true)
+                    .doOnSubscribe(s -> {
+                        Account account = mAccountService.getAccount(accountId);
+                        account.clearHistory(conversationUri, true);
+                        mAccountService.removeContact(accountId, conversationUri.getRawRingId(), false);
+                    });
+        }
     }
 
+    public void banConversation(String accountId, Uri conversationUri) {
+        if (conversationUri.isSwarm()) {
+            startConversation(accountId, conversationUri)
+                    .subscribe(conversation -> {
+                        try {
+                            Contact contact = conversation.getContact();
+                            mAccountService.removeContact(accountId, contact.getUri().getRawUriString(), true);
+                        } catch (Exception e) {
+                            mAccountService.removeConversation(accountId, conversationUri);
+                        }
+                    });
+            //return mAccountService.removeConversation(accountId, conversationUri);
+        } else {
+            mAccountService.removeContact(accountId, conversationUri.getRawUriString(), true);
+        }
+    }
+
+
     public Single<Conversation> createConversation(String accountId, Collection<Contact> currentSelection) {
         List<String> contactIds = new ArrayList<>(currentSelection.size());
         for (Contact contact : currentSelection)
             contactIds.add(contact.getPrimaryNumber());
         return mAccountService.startConversation(accountId, contactIds);
     }
-
-    public void loadMore(Conversation conversation) {
-        Collection<String> roots = conversation.getSwarmRoot();
-        if (roots.isEmpty())
-            mAccountService.loadConversationHistory(conversation.getAccountId(), conversation.getUri(), "", 16);
-        else {
-            for (String root : roots)
-                mAccountService.loadConversationHistory(conversation.getAccountId(), conversation.getUri(), root, 16);
-        }
-    }
 }
\ No newline at end of file
diff --git a/ring-android/libringclient/src/main/java/net/jami/model/Account.java b/ring-android/libringclient/src/main/java/net/jami/model/Account.java
index 3e5d703dd..96e1e0927 100644
--- a/ring-android/libringclient/src/main/java/net/jami/model/Account.java
+++ b/ring-android/libringclient/src/main/java/net/jami/model/Account.java
@@ -132,16 +132,25 @@ public class Account {
     }
 
     public void removeSwarm(String conversationId) {
+        Log.w(TAG, "removeSwarm " + conversationId);
         Conversation conversation = swarmConversations.remove(conversationId);
         if (conversation != null) {
             synchronized (conversations) {
-                conversations.remove(conversation.getUri().getUri());
+                Conversation c = conversations.remove(conversation.getUri().getUri());
+                try {
+                    Contact contact = c.getContact();
+                    Log.w(TAG, "removeSwarm: adding back contact conversation " + contact + " " + contact.getConversationUri().blockingFirst() + " " + c.getUri());
+                    if (contact.getConversationUri().blockingFirst().equals(c.getUri()))  {
+                        contact.setConversationUri(contact.getUri());
+                        contactAdded(contact);
+                    }
+                } catch (Exception ignored) {
+                }
             }
             conversationChanged();
         }
     }
 
-
     public static class ContactLocation {
         public double latitude;
         public double longitude;
@@ -913,8 +922,8 @@ public class Account {
     private void contactAdded(Contact contact) {
         Uri uri = contact.getUri();
         String key = uri.getUri();
-        //Log.w(TAG, "contactAdded " + getAccountID() + " " + key + " " + contact.getConversationUri());
-        if (!contact.getConversationUri().blockingFirst().equals(contact.getUri())) {
+        //Log.w(TAG, "contactAdded " + getAccountID() + " " + uri + " " + contact.getConversationUri().blockingFirst());
+        if (!contact.getConversationUri().blockingFirst().equals(uri)) {
             // Don't add conversation if we have a swarm conversation
             return;
         }
diff --git a/ring-android/libringclient/src/main/java/net/jami/model/Call.java b/ring-android/libringclient/src/main/java/net/jami/model/Call.java
index 3a3c52bc9..0e9a14cd8 100644
--- a/ring-android/libringclient/src/main/java/net/jami/model/Call.java
+++ b/ring-android/libringclient/src/main/java/net/jami/model/Call.java
@@ -22,6 +22,7 @@ package net.jami.model;
 import com.google.gson.JsonElement;
 import com.google.gson.JsonObject;
 
+import net.jami.utils.Log;
 import net.jami.utils.ProfileChunk;
 import net.jami.utils.StringUtils;
 import net.jami.utils.VCardUtils;
@@ -35,6 +36,7 @@ import ezvcard.Ezvcard;
 import ezvcard.VCard;
 
 public class Call extends Interaction {
+    public final static String TAG = Call.class.getSimpleName();
 
     public final static String KEY_ACCOUNT_ID = "ACCOUNTID";
     public final static String KEY_AUDIO_ONLY = "AUDIO_ONLY";
@@ -50,6 +52,8 @@ public class Call extends Interaction {
     public final static String KEY_DURATION = "duration";
     public final static String KEY_CONF_ID = "CONF_ID";
 
+    private final String mIdDaemon;
+
     private boolean isPeerHolding = false;
     private boolean isAudioMuted = false;
     private boolean isVideoMuted = false;
@@ -69,7 +73,12 @@ public class Call extends Interaction {
     private ProfileChunk mProfileChunk = null;
 
     public Call(String daemonId, String author, String account, ConversationHistory conversation, Contact contact, Direction direction) {
-        mDaemonId = daemonId == null ? null : Long.parseLong(daemonId);
+        mIdDaemon = daemonId;
+        try {
+            mDaemonId = daemonId == null ? null : Long.parseLong(daemonId);
+        } catch (Exception e) {
+            Log.e(TAG, "Can't parse CallId " + mDaemonId);
+        }
         mAuthor = direction == Direction.INCOMING ? author : null;
         mAccount = account;
         mConversation = conversation;
@@ -89,6 +98,7 @@ public class Call extends Interaction {
         mType = InteractionType.CALL.toString();
         mStatus = interaction.getStatus().toString();
         mDaemonId = interaction.getDaemonId();
+        mIdDaemon = super.getDaemonIdString();
         mIsRead = interaction.isRead() ? 1 : 0;
         mAccount = interaction.getAccount();
         mExtraFlag = fromJson(interaction.getExtraFlag());
@@ -98,12 +108,17 @@ public class Call extends Interaction {
     }
 
     public Call(String daemonId, String account, String contactNumber, Direction direction, long timestamp) {
-        mDaemonId = daemonId == null ? null : Long.parseLong(daemonId);
+        mIdDaemon = daemonId;
+        try {
+            mDaemonId = daemonId == null ? null : Long.parseLong(daemonId);
+        } catch (Exception e) {
+            Log.e(TAG, "Can't parse CallId " + mDaemonId);
+        }
         mIsIncoming = direction == Direction.INCOMING;
         mAccount = account;
         mAuthor = direction == Direction.INCOMING ? contactNumber : null;
         mContactNumber = contactNumber;
-        mTimestamp = System.currentTimeMillis();
+        mTimestamp = timestamp;
         mType = InteractionType.CALL.toString();
         mIsRead = 1;
     }
@@ -125,6 +140,11 @@ public class Call extends Interaction {
         mConfId = StringUtils.isEmpty(confId) ? null : confId;
     }
 
+    @Override
+    public String getDaemonIdString() {
+        return mIdDaemon;
+    }
+
     public boolean isConferenceParticipant() {
         return mConfId != null;
     }
@@ -249,12 +269,12 @@ public class Call extends Interaction {
 
     public VCard appendToVCard(Map<String, String> messages) {
         for (Map.Entry<String, String> message : messages.entrySet()) {
-            HashMap<String, String> messageKeyValue = net.jami.utils.VCardUtils.parseMimeAttributes(message.getKey());
-            String mimeType = messageKeyValue.get(net.jami.utils.VCardUtils.VCARD_KEY_MIME_TYPE);
-            if (!net.jami.utils.VCardUtils.MIME_PROFILE_VCARD.equals(mimeType)) {
+            HashMap<String, String> messageKeyValue = VCardUtils.parseMimeAttributes(message.getKey());
+            String mimeType = messageKeyValue.get(VCardUtils.VCARD_KEY_MIME_TYPE);
+            if (!VCardUtils.MIME_PROFILE_VCARD.equals(mimeType)) {
                 continue;
             }
-            int part = Integer.parseInt(messageKeyValue.get(net.jami.utils.VCardUtils.VCARD_KEY_PART));
+            int part = Integer.parseInt(messageKeyValue.get(VCardUtils.VCARD_KEY_PART));
             int nbPart = Integer.parseInt(messageKeyValue.get(VCardUtils.VCARD_KEY_OF));
             if (null == mProfileChunk) {
                 mProfileChunk = new ProfileChunk(nbPart);
diff --git a/ring-android/libringclient/src/main/java/net/jami/model/Contact.java b/ring-android/libringclient/src/main/java/net/jami/model/Contact.java
index fdecb7a18..996a40607 100644
--- a/ring-android/libringclient/src/main/java/net/jami/model/Contact.java
+++ b/ring-android/libringclient/src/main/java/net/jami/model/Contact.java
@@ -89,7 +89,6 @@ public class Contact {
 
     public void setConversationUri(Uri conversationUri) {
         mConversationUri.onNext(conversationUri);
-        //mConversationUri = conversationUri;
     }
 
     public Observable<Uri> getConversationUri() {
@@ -143,10 +142,9 @@ public class Contact {
     }
 
     public void setOnline(boolean present) {
+        mOnline = present;
         if (mContactPresenceEmitter != null)
             mContactPresenceEmitter.onNext(present);
-        mOnline = present;
-        mContactUpdates.onNext(this);
     }
 
     public void setSystemId(long id) {
diff --git a/ring-android/libringclient/src/main/java/net/jami/model/Conversation.java b/ring-android/libringclient/src/main/java/net/jami/model/Conversation.java
index b716d051e..37a24d1a1 100644
--- a/ring-android/libringclient/src/main/java/net/jami/model/Conversation.java
+++ b/ring-android/libringclient/src/main/java/net/jami/model/Conversation.java
@@ -36,10 +36,12 @@ import java.util.NavigableMap;
 import java.util.Set;
 import java.util.TreeMap;
 
+import io.reactivex.Completable;
 import io.reactivex.Observable;
 import io.reactivex.Single;
 import io.reactivex.subjects.BehaviorSubject;
 import io.reactivex.subjects.PublishSubject;
+import io.reactivex.subjects.SingleSubject;
 import io.reactivex.subjects.Subject;
 
 public class Conversation extends ConversationHistory {
@@ -64,9 +66,9 @@ public class Conversation extends ConversationHistory {
     private final Subject<List<Contact>> mContactSubject = BehaviorSubject.create();
 
     private Single<Conversation> isLoaded = null;
+    private Completable lastElementLoaded = null;
 
     private final Set<String> mRoots = new HashSet<>(2);
-    private final Set<String> mBranches = new HashSet<>(2);
     private final Map<String, Interaction> mMessages = new HashMap<>(16);
     private String lastRead = null;
     private final Mode mMode;
@@ -76,6 +78,7 @@ public class Conversation extends ConversationHistory {
 
     // indicate the list needs sorting
     private boolean mDirty = false;
+    private SingleSubject<Conversation> mLoadingSubject = null;
 
     public Conversation(String accountId, Contact contact) {
         mAccountId = accountId;
@@ -143,7 +146,6 @@ public class Conversation extends ConversationHistory {
         } else if (mContacts.size() == 1) {
             return mContacts.get(0).getDisplayName();
         }
-        StringBuilder ret = new StringBuilder();
         ArrayList<String> names = new ArrayList<>(mContacts.size());
         int target = mContacts.size();
         for (Contact c : mContacts) {
@@ -152,12 +154,13 @@ public class Conversation extends ConversationHistory {
                 continue;
             }
             String displayName = c.getDisplayName();
-            if (!net.jami.utils.StringUtils.isEmpty(displayName)) {
+            if (!StringUtils.isEmpty(displayName)) {
                 names.add(displayName);
                 if (names.size() == 3)
                     break;
             }
         }
+        StringBuilder ret = new StringBuilder();
         ret.append(StringUtils.join(", ", names));
         if (!names.isEmpty() && names.size() < target) {
             ret.append(" + ").append(mContacts.size() - names.size());
@@ -172,37 +175,33 @@ public class Conversation extends ConversationHistory {
         } else if (mContacts.size() == 1) {
             return mContacts.get(0).getRingUsername();
         }
-        StringBuilder ret = new StringBuilder();
-        Iterator<Contact> it = mContacts.iterator();
-        while (it.hasNext()) {
-            Contact c = it.next();
+        ArrayList<String> names = new ArrayList<>(mContacts.size());
+        for (Contact c : mContacts) {
             if (c.isUser())
                 continue;
-            ret.append(c.getRingUsername());
-            if (it.hasNext())
-                ret.append(", ");
+            names.add(c.getRingUsername());
         }
-        return ret.toString();
+        return StringUtils.join(", ", names);
     }
 
     public Observable<List<Contact>> getContactUpdates() {
         return mContactSubject;
     }
 
-    public String readMessages() {
+    public synchronized String readMessages() {
         Interaction interaction = null;
-        for (String branch : mBranches) {
-            Interaction i = mMessages.get(branch);
+        //for (String branch : mBranches) {
+            Interaction i = mAggregateHistory.get(mAggregateHistory.size() - 1);
             if (i != null && !i.isRead()) {
                 i.read();
                 interaction = i;
                 lastRead = i.getMessageId();
             }
-        }
+        //}
         return interaction == null ? null : interaction.getMessageId();
     }
 
-    public Interaction getMessage(String messageId) {
+    public synchronized Interaction getMessage(String messageId) {
         return mMessages.get(messageId);
     }
 
@@ -214,11 +213,41 @@ public class Conversation extends ConversationHistory {
         return lastRead;
     }
 
+    public SingleSubject<Conversation> getLoading() {
+        return mLoadingSubject;
+    }
+
+    public boolean stopLoading() {
+        SingleSubject<Conversation> ret = mLoadingSubject;
+        mLoadingSubject = null;
+        if (ret != null) {
+            ret.onSuccess(this);
+            return true;
+        }
+        return false;
+    }
+
+    public void setLoading(SingleSubject<Conversation> l) {
+        if (mLoadingSubject != null) {
+            if (!mLoadingSubject.hasValue() && !mLoadingSubject.hasThrowable())
+                mLoadingSubject.onError(new IllegalStateException());
+        }
+        mLoadingSubject = l;
+    }
+
+    public Completable getLastElementLoaded() {
+        return lastElementLoaded;
+    }
+
+    public void setLastElementLoaded(Completable c) {
+        lastElementLoaded = c;
+    }
+
     public enum ElementStatus {
         UPDATE, REMOVE, ADD
     }
 
-    public Observable<net.jami.utils.Tuple<Interaction, ElementStatus>> getUpdatedElements() {
+    public Observable<Tuple<Interaction, ElementStatus>> getUpdatedElements() {
         return updatedElementSubject;
     }
 
@@ -301,7 +330,7 @@ public class Conversation extends ConversationHistory {
         }
         mDirty = true;
         mAggregateHistory.add(call);
-        updatedElementSubject.onNext(new net.jami.utils.Tuple<>(call, ElementStatus.ADD));
+        updatedElementSubject.onNext(new Tuple<>(call, ElementStatus.ADD));
     }
 
     private void setInteractionProperties(Interaction interaction) {
@@ -328,13 +357,13 @@ public class Conversation extends ConversationHistory {
             txt.read();
         }
         if (txt.getConversation() == null) {
-            net.jami.utils.Log.e(TAG, "Error in conversation class... No conversation is attached to this interaction");
+            Log.e(TAG, "Error in conversation class... No conversation is attached to this interaction");
         }
         setInteractionProperties(txt);
         mHistory.put(txt.getTimestamp(), txt);
         mDirty = true;
         mAggregateHistory.add(txt);
-        updatedElementSubject.onNext(new net.jami.utils.Tuple<>(txt, ElementStatus.ADD));
+        updatedElementSubject.onNext(new Tuple<>(txt, ElementStatus.ADD));
     }
 
     public void addRequestEvent(TrustRequest request, Contact contact) {
@@ -343,20 +372,20 @@ public class Conversation extends ConversationHistory {
         ContactEvent event = new ContactEvent(contact, request);
         mDirty = true;
         mAggregateHistory.add(event);
-        updatedElementSubject.onNext(new net.jami.utils.Tuple<>(event, ElementStatus.ADD));
+        updatedElementSubject.onNext(new Tuple<>(event, ElementStatus.ADD));
     }
 
     public void addContactEvent(Contact contact) {
         ContactEvent event = new ContactEvent(contact);
         mDirty = true;
         mAggregateHistory.add(event);
-        updatedElementSubject.onNext(new net.jami.utils.Tuple<>(event, ElementStatus.ADD));
+        updatedElementSubject.onNext(new Tuple<>(event, ElementStatus.ADD));
     }
 
     public void addContactEvent(ContactEvent contactEvent) {
         mDirty = true;
         mAggregateHistory.add(contactEvent);
-        updatedElementSubject.onNext(new net.jami.utils.Tuple<>(contactEvent, ElementStatus.ADD));
+        updatedElementSubject.onNext(new Tuple<>(contactEvent, ElementStatus.ADD));
     }
 
     public void addFileTransfer(DataTransfer dataTransfer) {
@@ -365,42 +394,56 @@ public class Conversation extends ConversationHistory {
         }
         mDirty = true;
         mAggregateHistory.add(dataTransfer);
-        updatedElementSubject.onNext(new net.jami.utils.Tuple<>(dataTransfer, ElementStatus.ADD));
+        updatedElementSubject.onNext(new Tuple<>(dataTransfer, ElementStatus.ADD));
     }
 
-    public void updateTextMessage(TextMessage text) {
+    boolean isAfter(Interaction previous, Interaction query) {
         if (isSwarm()) {
-            TextMessage txt = (TextMessage) mMessages.get(text.getMessageId());
-            if (txt != null) {
-                txt.setStatus(text.getStatus());
-                updatedElementSubject.onNext(new net.jami.utils.Tuple<>(txt, ElementStatus.UPDATE));
-                if (text.getStatus() == Interaction.InteractionStatus.DISPLAYED) {
-                    if (lastDisplayed == null || lastDisplayed.getTimestamp() < text.getTimestamp()) {
-                        lastDisplayed = text;
-                        lastDisplayedSubject.onNext(text);
+            while (query != null && query.getParentIds() != null && !query.getParentIds().isEmpty()) {
+                if (query.getParentIds().contains(previous.getMessageId()))
+                    return true;
+                query = mMessages.get(query.getParentIds().get(0));
+            }
+            return false;
+        } else {
+            return previous.getTimestamp() < query.getTimestamp();
+        }
+    }
+
+    public void updateInteraction(Interaction element) {
+        Log.e(TAG, "updateInteraction: " + element.getMessageId() + " " + element.getStatus());
+        if (isSwarm()) {
+            Interaction e = mMessages.get(element.getMessageId());
+            if (e != null) {
+                e.setStatus(element.getStatus());
+                updatedElementSubject.onNext(new Tuple<>(e, ElementStatus.UPDATE));
+                if (e.getStatus() == Interaction.InteractionStatus.DISPLAYED) {
+                    if (lastDisplayed == null || isAfter(lastDisplayed, e)) {
+                        lastDisplayed = e;
+                        lastDisplayedSubject.onNext(e);
                     }
                 }
             } else {
-                net.jami.utils.Log.e(TAG, "Can't find swarm message to update: " + text.getMessageId());
+                Log.e(TAG, "Can't find swarm message to update: " + element.getMessageId());
             }
         } else {
-            setInteractionProperties(text);
-            long time = text.getTimestamp();
+            setInteractionProperties(element);
+            long time = element.getTimestamp();
             NavigableMap<Long, Interaction> msgs = mHistory.subMap(time, true, time, true);
             for (Interaction txt : msgs.values()) {
-                if (txt.getId() == text.getId()) {
-                    txt.setStatus(text.getStatus());
-                    updatedElementSubject.onNext(new net.jami.utils.Tuple<>(txt, ElementStatus.UPDATE));
-                    if (text.getStatus() == Interaction.InteractionStatus.DISPLAYED) {
-                        if (lastDisplayed == null || lastDisplayed.getTimestamp() < text.getTimestamp()) {
-                            lastDisplayed = text;
-                            lastDisplayedSubject.onNext(text);
+                if (txt.getId() == element.getId()) {
+                    txt.setStatus(element.getStatus());
+                    updatedElementSubject.onNext(new Tuple<>(txt, ElementStatus.UPDATE));
+                    if (element.getStatus() == Interaction.InteractionStatus.DISPLAYED) {
+                        if (lastDisplayed == null || isAfter(lastDisplayed, element)) {
+                            lastDisplayed = element;
+                            lastDisplayedSubject.onNext(element);
                         }
                     }
                     return;
                 }
             }
-            net.jami.utils.Log.e(TAG, "Can't find message to update: " + text.getId());
+            Log.e(TAG, "Can't find message to update: " + element.getId());
         }
     }
 
@@ -429,9 +472,7 @@ public class Conversation extends ConversationHistory {
 
     public Interaction getLastEvent() {
         sortHistory();
-        if (mAggregateHistory.isEmpty())
-            return null;
-        return mAggregateHistory.get(mAggregateHistory.size() - 1);
+        return mAggregateHistory.isEmpty() ? null : mAggregateHistory.get(mAggregateHistory.size() - 1);
     }
 
     public Conference getCurrentCall() {
@@ -579,53 +620,67 @@ public class Conversation extends ConversationHistory {
         }
     }
 
-    private boolean isNewLeaf(List<String> roots) {
-        if (mBranches.isEmpty())
-            return true;
-        boolean addLeaf = false;
-        for (String root : roots) {
-            if (mBranches.remove(root))
-                addLeaf = true;
-        }
-        return addLeaf;
-    }
-
     public boolean addSwarmElement(Interaction interaction) {
         if (mMessages.containsKey(interaction.getMessageId())) {
             return false;
         }
-        boolean newMessage = false;
         mMessages.put(interaction.getMessageId(), interaction);
-        if (mRoots.isEmpty() || mRoots.contains(interaction.getMessageId())) {
-            mRoots.remove(interaction.getMessageId());
-            mRoots.addAll(interaction.getParentIds());
-            // Log.w(TAG, "Found new roots for " + getUri() + " " + mRoots);
-        }
+        mRoots.remove(interaction.getMessageId());
+        for (String parent : interaction.getParentIds())
+            if (!mMessages.containsKey(parent)) {
+                mRoots.add(parent);
+                // Log.w(TAG, "@@@ Found new root for " + getUri() + " " + parent + " -> " + mRoots);
+            }
         if (lastRead != null && lastRead.equals(interaction.getMessageId()))
             interaction.read();
-        if (isNewLeaf(interaction.getParentIds())) {
-            mBranches.add(interaction.getMessageId());
-            if (isVisible()) {
-                interaction.read();
-                setLastMessageRead(interaction.getMessageId());
-            }
-            newMessage = true;
-        }
+        boolean newLeaf = false;
+        boolean added = false;
         if (mAggregateHistory.isEmpty() || interaction.getParentIds().contains(mAggregateHistory.get(mAggregateHistory.size()-1).getMessageId())) {
             // New leaf
+            // Log.w(TAG, "@@@ New end LEAF");
+            added = true;
+            newLeaf = true;
             mAggregateHistory.add(interaction);
-            updatedElementSubject.onNext(new net.jami.utils.Tuple<>(interaction, ElementStatus.ADD));
+            updatedElementSubject.onNext(new Tuple<>(interaction, ElementStatus.ADD));
         } else {
             // New root or normal node
             for (int i = 0; i < mAggregateHistory.size(); i++) {
                 if (mAggregateHistory.get(i).getParentIds() != null && mAggregateHistory.get(i).getParentIds().contains(interaction.getMessageId())) {
+                    //Log.w(TAG, "@@@ New root node at " + i);
                     mAggregateHistory.add(i, interaction);
-                    updatedElementSubject.onNext(new net.jami.utils.Tuple<>(interaction, ElementStatus.ADD));
+                    updatedElementSubject.onNext(new Tuple<>(interaction, ElementStatus.ADD));
+                    added = true;
                     break;
                 }
             }
+            if (!added) {
+                for (int i = mAggregateHistory.size()-1; i >= 0; i--) {
+                    if (interaction.getParentIds().contains(mAggregateHistory.get(i).getMessageId())) {
+                        //Log.w(TAG, "@@@ New leaf at " + (i+1));
+                        added = true;
+                        newLeaf = true;
+                        mAggregateHistory.add(i+1, interaction);
+                        updatedElementSubject.onNext(new Tuple<>(interaction, ElementStatus.ADD));
+                        break;
+                    }
+                }
+
+            }
         }
-        return newMessage;
+        if (newLeaf) {
+            if (isVisible()) {
+                interaction.read();
+                setLastMessageRead(interaction.getMessageId());
+            }
+        }
+        if (!added) {
+            Log.e(TAG, "Can't attach interaction " + interaction.getMessageId() + " with parents " + interaction.getParentIds());
+        }
+        return newLeaf;
+    }
+
+    public boolean isLoaded()  {
+        return !mMessages.isEmpty() && mRoots.isEmpty();
     }
 
     public Collection<String> getSwarmRoot() {
@@ -636,7 +691,7 @@ public class Conversation extends ConversationHistory {
         DataTransfer dataTransfer = (DataTransfer) findConversationElement(transfer.getId());
         if (dataTransfer != null) {
             dataTransfer.setStatus(eventCode);
-            updatedElementSubject.onNext(new net.jami.utils.Tuple<>(dataTransfer, ElementStatus.UPDATE));
+            updatedElementSubject.onNext(new Tuple<>(dataTransfer, ElementStatus.UPDATE));
         }
     }
 
diff --git a/ring-android/libringclient/src/main/java/net/jami/model/DataTransfer.java b/ring-android/libringclient/src/main/java/net/jami/model/DataTransfer.java
index 93a104521..079d8793f 100644
--- a/ring-android/libringclient/src/main/java/net/jami/model/DataTransfer.java
+++ b/ring-android/libringclient/src/main/java/net/jami/model/DataTransfer.java
@@ -45,7 +45,6 @@ public class DataTransfer extends Interaction {
         mAuthor = isOutgoing ? null : peer;
         mAccount = account;
         mConversation = conversation;
-        mAuthor = peer;
         mTotalSize = totalSize;
         mBytesProgress = bytesProgress;
         mBody = displayName;
@@ -115,13 +114,18 @@ public class DataTransfer extends Interaction {
     }
 
     public String getStoragePath() {
-        if (net.jami.utils.StringUtils.isEmpty(mBody)) {
+        if (StringUtils.isEmpty(mBody)) {
             return getMessageId();
+        } else {
+            String ext = StringUtils.getFileExtension(mBody);
+            if (ext.length() > 8)
+                ext = ext.substring(0, 8);
+            if (mDaemonId == null || mDaemonId == 0) {
+                return Long.toString(mId) + '_' + HashUtils.sha1(mBody) + '.' + ext;
+            } else {
+                return Long.toString(mDaemonId) + '_' + HashUtils.sha1(mBody) + '.' + ext;
+            }
         }
-        String ext = StringUtils.getFileExtension(mBody);
-        if (ext.length() > 8)
-            ext = ext.substring(0, 8);
-        return Long.toString(mId) + '_' + HashUtils.sha1(mBody) + '.' + ext;
     }
 
     public void setSize(long size) {
diff --git a/ring-android/libringclient/src/main/java/net/jami/model/Interaction.java b/ring-android/libringclient/src/main/java/net/jami/model/Interaction.java
index 07bfc9d14..b4608a6e7 100644
--- a/ring-android/libringclient/src/main/java/net/jami/model/Interaction.java
+++ b/ring-android/libringclient/src/main/java/net/jami/model/Interaction.java
@@ -73,6 +73,16 @@ public class Interaction {
     /* Needed by ORMLite */
     public Interaction() {
     }
+    public Interaction(String accountId) {
+        mAccount = accountId;
+        setType(InteractionType.INVALID);
+    }
+
+    public Interaction(Conversation conversation, InteractionType type) {
+        mConversation = conversation;
+        mAccount = conversation.getAccountId();
+        mType = type.toString();
+    }
 
     public Interaction(String id, String author, ConversationHistory conversation, String timestamp, String body, String type, String status, String daemonId, String isRead, String extraFlag) {
         mId = Integer.parseInt(id);
@@ -152,6 +162,8 @@ public class Interaction {
     }
 
     public void setStatus(InteractionStatus status) {
+        if (status == InteractionStatus.DISPLAYED)
+            mIsRead = 1;
         mStatus = status.toString();
     }
 
@@ -175,6 +187,10 @@ public class Interaction {
         return mDaemonId == null ? null : Long.toString(mDaemonId);
     }
 
+    public void setDaemonId(long daemonId) {
+        mDaemonId = daemonId;
+    }
+
     public String getMessageId() {
         return mMessageId;
     }
@@ -237,7 +253,7 @@ public class Interaction {
             return INVALID;
         }
 
-        static InteractionStatus fromIntTextMessage(int n) {
+        public static InteractionStatus fromIntTextMessage(int n) {
             try {
                 return values()[n];
             } catch (ArrayIndexOutOfBoundsException e) {
diff --git a/ring-android/libringclient/src/main/java/net/jami/model/TextMessage.java b/ring-android/libringclient/src/main/java/net/jami/model/TextMessage.java
index 20053a668..e9a4d87cd 100644
--- a/ring-android/libringclient/src/main/java/net/jami/model/TextMessage.java
+++ b/ring-android/libringclient/src/main/java/net/jami/model/TextMessage.java
@@ -77,13 +77,4 @@ public class TextMessage extends Interaction {
     public void setNotified(boolean notified) {
         mNotified = notified;
     }
-
-    public void setStatus(int status) {
-        if (status == 3)
-            mIsRead = 1;
-
-        mStatus = InteractionStatus.fromIntTextMessage(status).toString();
-    }
-
-
 }
diff --git a/ring-android/libringclient/src/main/java/net/jami/services/AccountService.java b/ring-android/libringclient/src/main/java/net/jami/services/AccountService.java
index 47d4e3c93..2d6323f90 100644
--- a/ring-android/libringclient/src/main/java/net/jami/services/AccountService.java
+++ b/ring-android/libringclient/src/main/java/net/jami/services/AccountService.java
@@ -59,7 +59,6 @@ import java.io.UnsupportedEncodingException;
 import java.net.SocketException;
 import java.net.URLEncoder;
 import java.util.ArrayList;
-import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.Date;
@@ -83,6 +82,7 @@ import io.reactivex.Single;
 import io.reactivex.schedulers.Schedulers;
 import io.reactivex.subjects.BehaviorSubject;
 import io.reactivex.subjects.PublishSubject;
+import io.reactivex.subjects.SingleSubject;
 import io.reactivex.subjects.Subject;
 
 /**
@@ -221,7 +221,7 @@ public class AccountService {
             })
             .share();
 
-    private final Subject<TextMessage> textMessageSubject = PublishSubject.create();
+    private final Subject<Interaction> messageSubject = PublishSubject.create();
     private final Subject<DataTransfer> dataTransferSubject = PublishSubject.create();
     private final Subject<TrustRequest> incomingRequestsSubject = PublishSubject.create();
 
@@ -309,8 +309,8 @@ public class AccountService {
         return incomingLocationSubject;
     }
 
-    public Observable<TextMessage> getMessageStateChanges() {
-        return textMessageSubject;
+    public Observable<Interaction> getMessageStateChanges() {
+        return messageSubject;
     }
 
     public Observable<TrustRequest> getIncomingRequests() {
@@ -402,11 +402,6 @@ public class AccountService {
                     Conversation conversation = account.newSwarm(conversationId, mode);
                     conversation.setLastMessageRead(mHistoryService.getLastMessageRead(accountId, conversation.getUri()));
                     for (Map<String, String> member : JamiService.getConversationMembers(accountId, conversationId)) {
-/*
-                        for (Map.Entry<String, String> minfo : member.entrySet()) {
-                            Log.w(TAG, accountId + " " + conversationId + " member " + minfo.getKey() + " -> " + minfo.getValue());
-                        }
-                        //conversation.addContact(account.getContactFromCache(member.get("uri")));*/
                         Uri uri = Uri.fromId(member.get("uri"));
                         Contact contact = conversation.findContact(uri);
                         if (contact == null) {
@@ -414,7 +409,7 @@ public class AccountService {
                             conversation.addContact(contact);
                         }
                     }
-                    Log.w(TAG, accountId + " " + conversation.getUri().getRawUriString() + " " + conversation.getContacts().size());
+                    conversation.setLastElementLoaded(Completable.defer(() -> loadMore(conversation, 2).ignoreElement()).cache());
                     account.conversationStarted(conversation);
                     //account.addSwarmConversation(conversationId, members);
                 }
@@ -431,10 +426,6 @@ public class AccountService {
                         account.addRequest(new TrustRequest(account.getAccountID(), from, conversationId));
                     }
                 }
-                mExecutor.execute(() -> {
-                    for (String conversationId : conversations)
-                        JamiService.loadConversationMessages(accountId, conversationId, "", 2);
-                });
 
                 if (enabled) {
                     for (Contact contact : account.getContacts().values()) {
@@ -565,8 +556,7 @@ public class AccountService {
                 .firstOrError()
                 .map(accounts -> {
                     for (Account account : accounts) {
-                        String accountID = account.getAccountID();
-                        if (accountID.equals(accountId)) {
+                        if (account.getAccountID().equals(accountId)) {
                             return account;
                         }
                     }
@@ -668,8 +658,8 @@ public class AccountService {
                 }, e -> Log.w(TAG, "Not sending empty profile", e));
     }
 
-    public void setMessageDisplayed(String accountId, String contactId, String messageId) {
-        mExecutor.execute(() -> JamiService.setMessageDisplayed(accountId, contactId, messageId, 3));
+    public void setMessageDisplayed(String accountId, Uri conversationUri, String messageId) {
+        mExecutor.execute(() -> JamiService.setMessageDisplayed(accountId, conversationUri.getUri(), messageId, 3));
     }
 
     public Single<Conversation> startConversation(String accountId, Collection<String> initialMembers) {
@@ -685,15 +675,49 @@ public class AccountService {
             }
             account.conversationStarted(conversation);
             Log.w(TAG, "loadConversationMessages");
-            JamiService.loadConversationMessages(accountId, id, id, 2);
+            //loadMore(conversation);
+            //JamiService.loadConversationMessages(accountId, id, id, 2);
             return conversation;
         }).subscribeOn(Schedulers.from(mExecutor));
     }
 
+    public Completable removeConversation(String accountId, Uri conversationUri) {
+        return Completable.fromAction(() -> JamiService.removeConversation(accountId, conversationUri.getRawRingId()))
+                .subscribeOn(Schedulers.from(mExecutor));
+    }
+
     public void loadConversationHistory(String accountId, Uri conversationUri, String root, long n) {
         JamiService.loadConversationMessages(accountId, conversationUri.getRawRingId(), root, n);
     }
 
+    public Single<Conversation> loadMore(Conversation conversation) {
+        return loadMore(conversation, 16);
+    }
+    public Single<Conversation> loadMore(Conversation conversation, int n) {
+        synchronized (conversation) {
+            if (conversation.isLoaded()) {
+                Log.w(TAG, "loadMore: conversation already fully loaded");
+                return Single.just(conversation);
+            }
+
+            SingleSubject<Conversation> ret = conversation.getLoading();
+            if (ret != null)
+                return ret;
+            ret = SingleSubject.create();
+            Collection<String> roots = conversation.getSwarmRoot();
+            Log.w(TAG, "loadMore " + conversation.getUri() + " " + roots);
+
+            conversation.setLoading(ret);
+            if (roots.isEmpty())
+                loadConversationHistory(conversation.getAccountId(), conversation.getUri(), "", n);
+            else {
+                for (String root : roots)
+                    loadConversationHistory(conversation.getAccountId(), conversation.getUri(), root, n);
+            }
+            return ret;
+        }
+    }
+
     public void sendConversationMessage(String accountId, Uri conversationUri, String txt) {
         mExecutor.execute(() -> {
             Log.w(TAG, "sendConversationMessages " + conversationUri.getRawRingId() + " : " + txt);
@@ -1316,16 +1340,17 @@ public class AccountService {
     }
 
     void accountMessageStatusChanged(String accountId, String conversationId, String messageId, String peer, int status) {
-        Log.d(TAG, "accountMessageStatusChanged: " + accountId + ", " + conversationId + ", " + messageId + ", " + peer + ", " + status);
+        InteractionStatus newStatus = InteractionStatus.fromIntTextMessage(status);
+        Log.d(TAG, "accountMessageStatusChanged: " + accountId + ", " + conversationId + ", " + messageId + ", " + peer + ", " + newStatus);
         if (StringUtils.isEmpty(conversationId)) {
             mHistoryService
-                    .accountMessageStatusChanged(accountId, messageId, peer, status)
-                    .subscribe(textMessageSubject::onNext, e -> Log.e(TAG, "Error updating message: " + e.getLocalizedMessage()));
+                    .accountMessageStatusChanged(accountId, messageId, peer, newStatus)
+                    .subscribe(messageSubject::onNext, e -> Log.e(TAG, "Error updating message: " + e.getLocalizedMessage()));
         } else {
-            TextMessage msg = new TextMessage(peer, accountId, messageId, null, null);
-            msg.setStatus(status);
+            Interaction msg = new Interaction(accountId);
+            msg.setStatus(newStatus);
             msg.setSwarmInfo(conversationId, messageId, null);
-            textMessageSubject.onNext(msg);
+            messageSubject.onNext(msg);
         }
     }
 
@@ -1492,11 +1517,13 @@ public class AccountService {
 
     private Interaction addMessage(Account account, Conversation conversation, Map<String, String> message)  {
         String id = message.get("id");
-        List<String> parents = Arrays.asList(message.get("parents").split(","));
-        if (parents.size() == 1 && parents.get(0).isEmpty())
-            parents = Collections.emptyList();
+        //List<String> parents = Arrays.asList(message.get("parents").split(","));
+        //if (parents.size() == 1 && parents.get(0).isEmpty())
+        //    parents = Collections.emptyList();
         String type = message.get("type");
         String author = message.get("author");
+        String parent = message.get("linearizedParent");
+        List<String> parents = StringUtils.isEmpty(parent) ? Collections.emptyList() : Collections.singletonList(parent);
         Uri authorUri = Uri.fromId(author);
 
         //Log.w(TAG, "addMessage2 " + type + " " + author + " id:" + id + " parents:" + parents);
@@ -1516,13 +1543,22 @@ public class AccountService {
                 interaction = new TextMessage(author, account.getAccountID(), timestamp, conversation, message.get("body"), !contact.isUser());
                 break;
             case "application/data-transfer+json": {
-                String transferId = message.get("tid");
-                String fileName = message.get("displayName");
-                long fileSize = Long.parseLong(message.get("totalSize"));
-                long tid = Long.parseUnsignedLong(transferId);
-                interaction = account.getDataTransfer(tid);
-                if (interaction == null) {
-                    interaction = new DataTransfer(tid, account.getAccountID(), author, fileName, contact.isUser(), timestamp, fileSize, 0);
+                try {
+                    String transferId = message.get("tid");
+                    long tid = Long.parseLong(transferId);
+                    String fileName = message.get("displayName");
+                    long fileSize = Long.parseLong(message.get("totalSize"));
+                    interaction = account.getDataTransfer(tid);
+                    if (interaction == null) {
+                        interaction = new DataTransfer(tid, account.getAccountID(), author, fileName, contact.isUser(), timestamp, fileSize, 0);
+                        File path = mDeviceRuntimeService.getConversationPath(conversation.getUri().getRawRingId(), ((DataTransfer) interaction).getStoragePath());
+                        boolean exists = path.exists();
+                        if (exists)
+                            ((DataTransfer) interaction).setBytesProgress(path.length());
+                        interaction.setStatus(exists ? InteractionStatus.TRANSFER_FINISHED : InteractionStatus.TRANSFER_TIMEOUT_EXPIRED);
+                    }
+                } catch (Exception e) {
+                    interaction = new Interaction(conversation, Interaction.InteractionType.INVALID);
                 }
                 break;
             }
@@ -1532,10 +1568,7 @@ public class AccountService {
                 break;
             case "merge":
             default:
-                interaction = new Interaction();
-                interaction.setAccount(account.getAccountID());
-                interaction.setConversation(conversation);
-                interaction.setType(Interaction.InteractionType.INVALID);
+                interaction = new Interaction(conversation, Interaction.InteractionType.INVALID);
                 break;
         }
         interaction.setContact(contact);
@@ -1549,15 +1582,18 @@ public class AccountService {
 
     public void conversationLoaded(String accountId, String conversationId, List<Map<String, String>> messages) {
         try {
-            Log.w(TAG, "ConversationCallback: conversationLoaded " + accountId + "/" + conversationId + " " + messages.size());
+            // Log.w(TAG, "ConversationCallback: conversationLoaded " + accountId + "/" + conversationId + " " + messages.size());
             Account account = getAccount(accountId);
             if (account == null) {
                 Log.w(TAG, "conversationLoaded: can't find account");
                 return;
             }
             Conversation conversation = account.getSwarm(conversationId);
-            for (Map<String, String> message : messages) {
-                addMessage(account, conversation, message);
+            synchronized (conversation) {
+                for (Map<String, String> message : messages) {
+                    addMessage(account, conversation, message);
+                }
+                conversation.stopLoading();
             }
             account.conversationChanged();
         } catch (Exception e) {
@@ -1581,8 +1617,10 @@ public class AccountService {
         switch (ConversationMemberEvent.values()[event])  {
             case Add:
             case Join: {
-                Contact contact = account.getContactFromCache(uri);
-                conversation.addContact(contact);
+                Contact contact = conversation.findContact(uri);
+                if (contact == null) {
+                    conversation.addContact(account.getContactFromCache(uri));
+                }
                 break;
             }
             case Remove:
@@ -1646,8 +1684,8 @@ public class AccountService {
         Log.w(TAG, "ConversationCallback: messageReceived " + accountId + "/" + conversationId + " " + message.size());
         Account account = getAccount(accountId);
         Conversation conversation = account.getSwarm(conversationId);
-        Interaction interaction = addMessage(account, conversation, message);
-        if (interaction != null) {
+        synchronized (conversation) {
+            Interaction interaction = addMessage(account, conversation, message);
             account.conversationUpdated(conversation);
             boolean isIncoming = !interaction.getContact().isUser();
             if (isIncoming) {
@@ -1656,22 +1694,32 @@ public class AccountService {
         }
     }
 
-    public Completable sendFile(final DataTransfer dataTransfer) {
-        return Completable.fromAction(() -> {
+    public Single<DataTransfer> sendFile(final File file, final DataTransfer dataTransfer) {
+        return Single.fromCallable(() -> {
             mStartingTransfer = dataTransfer;
 
             DataTransferInfo dataTransferInfo = new DataTransferInfo();
             dataTransferInfo.setAccountId(dataTransfer.getAccount());
-            dataTransferInfo.setConversationId(dataTransfer.getConversationId());
-            dataTransferInfo.setPeer(dataTransfer.getConversationId());
-            dataTransferInfo.setPath(dataTransfer.destination.getAbsolutePath());
+
+            String conversationId = dataTransfer.getConversationId();
+            if (!StringUtils.isEmpty(conversationId))
+                dataTransferInfo.setConversationId(conversationId);
+            else
+                dataTransferInfo.setPeer(dataTransfer.getConversation().getParticipant());
+
+            dataTransferInfo.setPath(file.getAbsolutePath());
             dataTransferInfo.setDisplayName(dataTransfer.getDisplayName());
 
             Log.i(TAG, "sendFile() id=" + dataTransfer.getId() + " accountId=" + dataTransferInfo.getAccountId() + ", peer=" + dataTransferInfo.getPeer() + ", filePath=" + dataTransferInfo.getPath());
-            DataTransferError err = getDataTransferError(JamiService.sendFile(dataTransferInfo, 0));
+            long[] id = new long[1];
+            DataTransferError err = getDataTransferError(JamiService.sendFile(dataTransferInfo, id));
             if (err != DataTransferError.SUCCESS) {
                 throw new IOException(err.name());
+            } else {
+                Log.e(TAG, "sendFile: got ID " + id[0]);
+                dataTransfer.setDaemonId(id[0]);
             }
+            return dataTransfer;
         }).subscribeOn(Schedulers.from(mExecutor));
     }
 
diff --git a/ring-android/libringclient/src/main/java/net/jami/services/CallService.java b/ring-android/libringclient/src/main/java/net/jami/services/CallService.java
index e5f7f0190..7a6d4e64c 100644
--- a/ring-android/libringclient/src/main/java/net/jami/services/CallService.java
+++ b/ring-android/libringclient/src/main/java/net/jami/services/CallService.java
@@ -508,16 +508,18 @@ public class CallService {
             }
 
             call.setCallState(callState);
+            Account account = mAccountService.getAccount(call.getAccount());
 
-            Contact contact = mContactService.findContact(mAccountService.getAccount(call.getAccount()), Uri.fromString(call.getContactNumber()));
+            Contact contact = mContactService.findContact(account, Uri.fromString(call.getContactNumber()));
             String registeredName = callDetails.get(Call.KEY_REGISTERED_NAME);
             if (registeredName != null && !registeredName.isEmpty()) {
                 contact.setUsername(registeredName);
             }
-            call.setContact(contact);
 
-            Account account = mAccountService.getAccount(call.getAccount());
-            call.setConversation(account.getByUri(contact.getUri()));
+            Conversation conversation = account.getByUri(contact.getConversationUri().blockingFirst());
+            call.setContact(contact);
+            call.setConversation(conversation);
+            Log.w(TAG, "parseCallState " + contact + " " + contact.getConversationUri().blockingFirst() + " " + conversation + " " + conversation.getParticipant());
 
             currentCalls.put(callId, call);
             updateConnectionCount();
@@ -525,7 +527,6 @@ public class CallService {
         return call;
     }
 
-
     public void connectionUpdate(String id, int state) {
         // Log.d(TAG, "connectionUpdate: " + id + " " + state);
         /*switch(state) {
diff --git a/ring-android/libringclient/src/main/java/net/jami/services/ContactService.java b/ring-android/libringclient/src/main/java/net/jami/services/ContactService.java
index f5a863a8f..c9f2c22b4 100644
--- a/ring-android/libringclient/src/main/java/net/jami/services/ContactService.java
+++ b/ring-android/libringclient/src/main/java/net/jami/services/ContactService.java
@@ -119,11 +119,14 @@ public abstract class ContactService {
                         })
                         .filter(c -> c.isUsernameLoaded() && c.detailsLoaded)
                         .replay(1)
-                        .refCount(5, TimeUnit.SECONDS));
+                        .refCount());
             }
 
             return withPresence
-                    ? Observable.combineLatest(contact.getUpdates(), contact.getPresenceUpdates(), (c, p) -> c)
+                    ? Observable.combineLatest(contact.getUpdates(), contact.getPresenceUpdates(), (c, p) -> {
+                //Log.w(TAG, "observeContact UPDATE " + c + " " + p);
+                return c;
+            })
                     : contact.getUpdates();
         }
     }
diff --git a/ring-android/libringclient/src/main/java/net/jami/services/DeviceRuntimeService.java b/ring-android/libringclient/src/main/java/net/jami/services/DeviceRuntimeService.java
index 92d718b22..4f551e896 100644
--- a/ring-android/libringclient/src/main/java/net/jami/services/DeviceRuntimeService.java
+++ b/ring-android/libringclient/src/main/java/net/jami/services/DeviceRuntimeService.java
@@ -31,6 +31,7 @@ public abstract class DeviceRuntimeService implements DaemonService.SystemInfoCa
     public abstract File getFilePath(String name);
     public abstract File getConversationPath(String conversationId, String name);
     public abstract File getTemporaryPath(String conversationId, String name);
+    public abstract File getConversationDir(String conversationId);
 
     public abstract String getPushToken();
 
@@ -56,4 +57,5 @@ public abstract class DeviceRuntimeService implements DaemonService.SystemInfoCa
 
     public abstract String getProfileName();
 
+    public abstract boolean hardLinkOrCopy(File source, File dest);
 }
diff --git a/ring-android/libringclient/src/main/java/net/jami/services/HistoryService.java b/ring-android/libringclient/src/main/java/net/jami/services/HistoryService.java
index 4579c2444..02ba0257a 100644
--- a/ring-android/libringclient/src/main/java/net/jami/services/HistoryService.java
+++ b/ring-android/libringclient/src/main/java/net/jami/services/HistoryService.java
@@ -220,7 +220,7 @@ public abstract class HistoryService {
     }
 
 
-    Single<TextMessage> accountMessageStatusChanged(String accountId, String daemonId, String peer, int status) {
+    Single<TextMessage> accountMessageStatusChanged(String accountId, String daemonId, String peer, Interaction.InteractionStatus status) {
         return Single.fromCallable(() -> {
             List<Interaction> textList = getInteractionDataDao(accountId).queryForEq(Interaction.COLUMN_DAEMON_ID, daemonId);
             if (textList == null || textList.isEmpty()) {
diff --git a/ring-android/libringclient/src/main/java/net/jami/services/NotificationService.java b/ring-android/libringclient/src/main/java/net/jami/services/NotificationService.java
index 2eb00b6f8..76e5f4d60 100644
--- a/ring-android/libringclient/src/main/java/net/jami/services/NotificationService.java
+++ b/ring-android/libringclient/src/main/java/net/jami/services/NotificationService.java
@@ -37,38 +37,30 @@ public interface NotificationService {
     String KEY_NOTIFICATION_ID = "notificationId";
 
     Object showCallNotification(int callId);
-
-    void showTextNotification(String accountId, Conversation conversation);
-
     void cancelCallNotification();
+    void handleCallNotification(Conference conference, boolean remove);
+    void showMissedCallNotification(Call call);
 
-    void cancelTextNotification(Uri contact);
-
-    void cancelTextNotification(String accountId, Uri conversationUri);
+    void showTextNotification(String accountId, Conversation conversation);
+    void cancelTextNotification(String accountId, Uri contact);
 
     void cancelAll();
 
     void showIncomingTrustRequestNotification(Account account);
-
     void cancelTrustRequestNotification(String accountID);
 
     void showFileTransferNotification(Conversation conversation, DataTransfer info);
-
-    void showMissedCallNotification(Call call);
-
     void cancelFileNotification(int id, boolean isMigratingToService);
+    void handleDataTransferNotification(DataTransfer transfer, Conversation contact, boolean remove);
+    void removeTransferNotification(String accountId, Uri conversationUri, long transferId);
+    Object getDataTransferNotification(int notificationId);
 
     void updateNotification(Object notification, int notificationId);
 
     Object getServiceNotification();
 
-    void handleCallNotification(Conference conference, boolean remove);
-
-    void handleDataTransferNotification(DataTransfer transfer, Conversation contact, boolean remove);
 
-    void removeTransferNotification(long transferId);
 
-    Object getDataTransferNotification(int notificationId);
 
     void onConnectionUpdate(Boolean b);
 
diff --git a/ring-android/libringclient/src/main/java/net/jami/services/VCardService.java b/ring-android/libringclient/src/main/java/net/jami/services/VCardService.java
index 1d3948371..ab078dc79 100644
--- a/ring-android/libringclient/src/main/java/net/jami/services/VCardService.java
+++ b/ring-android/libringclient/src/main/java/net/jami/services/VCardService.java
@@ -23,6 +23,7 @@ import java.io.File;
 
 import net.jami.utils.Tuple;
 import ezvcard.VCard;
+import io.reactivex.Maybe;
 import io.reactivex.Single;
 
 public abstract class VCardService {
@@ -30,7 +31,12 @@ public abstract class VCardService {
     public static final int MAX_SIZE_SIP = 256 * 1024;
     public static final int MAX_SIZE_REQUEST = 16 * 1024;
 
-    public abstract Single<VCard> loadSmallVCard(String accountId, int maxSize);
+    public abstract Maybe<VCard> loadSmallVCard(String accountId, int maxSize);
+    public Single<VCard> loadSmallVCardWithDefault(String accountId, int maxSize) {
+        return loadSmallVCard(accountId, maxSize)
+                .switchIfEmpty(Single.fromCallable(VCard::new));
+    }
+
     public abstract Single<VCard> saveVCardProfile(String accountId, String uri, String displayName, String picture);
     public abstract Single<Tuple<String, Object>> loadVCardProfile(VCard vcard);
     public abstract Single<Tuple<String, Object>> peerProfileReceived(String accountId, String peerId, File vcard);
diff --git a/ring-android/libringclient/src/main/java/net/jami/smartlist/SmartListPresenter.java b/ring-android/libringclient/src/main/java/net/jami/smartlist/SmartListPresenter.java
index 88c597e1a..a643e2427 100644
--- a/ring-android/libringclient/src/main/java/net/jami/smartlist/SmartListPresenter.java
+++ b/ring-android/libringclient/src/main/java/net/jami/smartlist/SmartListPresenter.java
@@ -20,13 +20,6 @@
  */
 package net.jami.smartlist;
 
-import java.util.ArrayList;
-import java.util.List;
-import java.util.concurrent.TimeUnit;
-
-import javax.inject.Inject;
-import javax.inject.Named;
-
 import net.jami.facades.ConversationFacade;
 import net.jami.model.Account;
 import net.jami.model.Uri;
@@ -34,6 +27,14 @@ import net.jami.mvp.RootPresenter;
 import net.jami.services.AccountService;
 import net.jami.services.ContactService;
 import net.jami.utils.Log;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+import javax.inject.Inject;
+import javax.inject.Named;
+
 import io.reactivex.Observable;
 import io.reactivex.Scheduler;
 import io.reactivex.disposables.CompositeDisposable;
@@ -101,11 +102,11 @@ public class SmartListPresenter extends RootPresenter<SmartListView> {
         }
     }
 
-    public void conversationClicked(net.jami.smartlist.SmartListViewModel viewModel) {
+    public void conversationClicked(SmartListViewModel viewModel) {
         startConversation(viewModel.getAccountId(), viewModel.getUri());
     }
 
-    public void conversationLongClicked(net.jami.smartlist.SmartListViewModel smartListViewModel) {
+    public void conversationLongClicked(SmartListViewModel smartListViewModel) {
         getView().displayConversationDialog(smartListViewModel);
     }
 
@@ -129,11 +130,11 @@ public class SmartListPresenter extends RootPresenter<SmartListView> {
         getView().goToConversation(mAccount.getAccountID(), uri);
     }
 
-    public void copyNumber(net.jami.smartlist.SmartListViewModel smartListViewModel) {
+    public void copyNumber(SmartListViewModel smartListViewModel) {
         getView().copyNumber(smartListViewModel.getUri());
     }
 
-    public void clearConversation(net.jami.smartlist.SmartListViewModel smartListViewModel) {
+    public void clearConversation(SmartListViewModel smartListViewModel) {
         getView().displayClearDialog(smartListViewModel.getUri());
     }
 
@@ -143,30 +144,33 @@ public class SmartListPresenter extends RootPresenter<SmartListView> {
                 .subscribeOn(Schedulers.computation()).subscribe());
     }
 
-    public void removeConversation(net.jami.smartlist.SmartListViewModel smartListViewModel) {
+    public void removeConversation(SmartListViewModel smartListViewModel) {
         getView().displayDeleteDialog(smartListViewModel.getUri());
     }
 
     public void removeConversation(Uri uri) {
         mConversationDisposable.add(mConversationFacade
                 .removeConversation(mAccount.getAccountID(), uri)
-                .subscribeOn(Schedulers.computation()).subscribe());
+                .subscribe());
     }
 
+    public void banContact(SmartListViewModel smartListViewModel) {
+        mConversationFacade.banConversation(smartListViewModel.getAccountId(), smartListViewModel.getUri());
+    }
     public void clickQRSearch() {
         getView().goToQRFragment();
     }
 
-    void showConversations(Observable<List<Observable<net.jami.smartlist.SmartListViewModel>>> conversations) {
+    void showConversations(Observable<List<Observable<SmartListViewModel>>> conversations) {
         mConversationDisposable.clear();
         getView().setLoading(true);
 
         mConversationDisposable.add(conversations
-                .switchMap(viewModels -> viewModels.isEmpty() ? net.jami.smartlist.SmartListViewModel.EMPTY_RESULTS
+                .switchMap(viewModels -> viewModels.isEmpty() ? SmartListViewModel.EMPTY_RESULTS
                         : Observable.combineLatest(viewModels, obs -> {
-                            List<net.jami.smartlist.SmartListViewModel> vms = new ArrayList<>(obs.length);
+                            List<SmartListViewModel> vms = new ArrayList<>(obs.length);
                             for (Object ob : obs)
-                                vms.add((net.jami.smartlist.SmartListViewModel) ob);
+                                vms.add((SmartListViewModel) ob);
                             return vms;
                         }))
                 .throttleLatest(150, TimeUnit.MILLISECONDS, mUiScheduler)
@@ -188,9 +192,4 @@ public class SmartListPresenter extends RootPresenter<SmartListView> {
         showConversations(mConversationFacade.getFullList(accountSubject, mCurrentQuery, true));
     }
 
-    public void banContact(SmartListViewModel smartListViewModel) {
-        //CallContact contact = smartListViewModel.getContact();
-        if (smartListViewModel.getContact().size() == 1)
-            mAccountService.removeContact(mAccount.getAccountID(), smartListViewModel.getContact().get(0).getPrimaryNumber(), true);
-    }
 }
diff --git a/ring-android/libringclient/src/main/java/net/jami/smartlist/SmartListViewModel.java b/ring-android/libringclient/src/main/java/net/jami/smartlist/SmartListViewModel.java
index bdf3210d0..2e9eccdfe 100644
--- a/ring-android/libringclient/src/main/java/net/jami/smartlist/SmartListViewModel.java
+++ b/ring-android/libringclient/src/main/java/net/jami/smartlist/SmartListViewModel.java
@@ -68,7 +68,7 @@ public class SmartListViewModel
         this.hasOngoingCall = false;
         this.lastEvent = lastEvent;
         showPresence = true;
-        //isOnline = contact.isOnline();
+        isOnline = contact.isOnline();
         title = Title.None;
     }
     public SmartListViewModel(String accountId, Contact contact, String id, Interaction lastEvent) {
@@ -102,7 +102,6 @@ public class SmartListViewModel
                 break;
             }
         }
-        //isOnline = contact.isOnline();
         showPresence = presence;
         title = Title.None;
     }
@@ -126,7 +125,7 @@ public class SmartListViewModel
         return uri;
     }
 
-    public List<Contact> getContact() {
+    public List<Contact> getContacts() {
         return contact;
     }
 
@@ -163,6 +162,10 @@ public class SmartListViewModel
         return showPresence;
     }
 
+    public boolean isOnline() {
+        return isOnline;
+    }
+
     public boolean isChecked() { return isChecked; }
 
     public void setChecked(boolean checked) {
diff --git a/ring-android/libringclient/src/main/java/net/jami/utils/VCardUtils.java b/ring-android/libringclient/src/main/java/net/jami/utils/VCardUtils.java
index 38fae1eac..b49650b31 100644
--- a/ring-android/libringclient/src/main/java/net/jami/utils/VCardUtils.java
+++ b/ring-android/libringclient/src/main/java/net/jami/utils/VCardUtils.java
@@ -166,7 +166,7 @@ public final class VCardUtils {
      */
     private static VCard loadFromDisk(File path) throws IOException {
         if (path == null || !path.exists()) {
-            Log.d(TAG, "vcardPath not exist " + path);
+            // Log.d(TAG, "vcardPath not exist " + path);
             return null;
         }
         if (path.length() > VCARD_MAX_SIZE) {
-- 
GitLab