diff --git a/build-daemon.sh b/build-daemon.sh
index 60cb2fd8edfb6985e2c0a70e18609f28220f3496..8a38aa3656e62cca626ff383994ee636db71873a 100755
--- a/build-daemon.sh
+++ b/build-daemon.sh
@@ -171,6 +171,7 @@ STATIC_LIBS_ALL="-llog -lOpenSLES -landroid \
                 -lpjlib-util-${PJ_TARGET} \
                 -lpj-${PJ_TARGET} \
                 -lupnp -lixml \
+                -lgit2 \
                 -larchive \
                 -lsecp256k1 \
                 -lgnutls -lhogweed -lnettle -lgmp \
diff --git a/ring-android/app/build.gradle b/ring-android/app/build.gradle
index e72dadbc5f8bec4b6637531150101f1bcfc0bb12..7956753bdd1a784bfe232269088ddfb207c6d4b4 100644
--- a/ring-android/app/build.gradle
+++ b/ring-android/app/build.gradle
@@ -107,7 +107,7 @@ dependencies {
     implementation 'com.google.zxing:core:3.3.3'
 
     // RxBindings
-    implementation 'com.jakewharton.rxbinding3:rxbinding:3.1.0'
+    //implementation 'com.jakewharton.rxbinding3:rxbinding:3.1.0'
 
     implementation 'com.rodolfonavalon:ShapeRippleLibrary:1.0.0'
 
diff --git a/ring-android/app/src/main/java/cx/ring/account/JamiAccountSummaryFragment.java b/ring-android/app/src/main/java/cx/ring/account/JamiAccountSummaryFragment.java
index 342294bf743a96f70aa96d9f4ce986cd6272e5b3..cfcbeda721c11bd06eba1964c1e0a6eb2cd9e9af 100644
--- a/ring-android/app/src/main/java/cx/ring/account/JamiAccountSummaryFragment.java
+++ b/ring-android/app/src/main/java/cx/ring/account/JamiAccountSummaryFragment.java
@@ -330,8 +330,8 @@ public class JamiAccountSummaryFragment extends BaseSupportFragment<JamiAccountS
         boolean hasRegisteredName = !currentRegisteredName && username != null && !username.isEmpty();
         mBinding.groupRegisteringName.setVisibility(currentRegisteredName ? View.VISIBLE : View.GONE);
         mBinding.btnShare.setOnClickListener(v -> shareAccount(hasRegisteredName? username : account.getUsername()));
-        mBinding.registerName.setVisibility(hasRegisteredName? View.GONE : View.VISIBLE);
-        mBinding.registeredName.setText(hasRegisteredName? username : getResources().getString(R.string.no_registered_name_for_account));
+        mBinding.registerName.setVisibility(hasRegisteredName ? View.GONE : View.VISIBLE);
+        mBinding.registeredName.setText(hasRegisteredName ? username : getResources().getString(R.string.no_registered_name_for_account));
         mBinding.btnQr.setOnClickListener(v -> QRCodeFragment.newInstance(QRCodeFragment.INDEX_CODE).show(getParentFragmentManager(), QRCodeFragment.TAG));
         mBinding.username.setOnFocusChangeListener((v, hasFocus) -> {
             Editable name = mBinding.username.getText();
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 9da488962cd31e90abcf5d23e626898c78c74b52..a28b6673cffeef6d2a48d1f290e98f223c44e8ca 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
@@ -32,6 +32,7 @@ import android.graphics.drawable.Drawable;
 import android.media.MediaPlayer;
 import android.net.Uri;
 import android.os.Build;
+import android.text.TextUtils;
 import android.text.format.DateUtils;
 import android.text.format.Formatter;
 import android.util.Log;
@@ -49,6 +50,7 @@ import android.view.animation.AnimationUtils;
 import android.widget.FrameLayout;
 import android.widget.ImageView;
 
+import androidx.annotation.LayoutRes;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.cardview.widget.CardView;
@@ -112,13 +114,13 @@ public class ConversationAdapter extends RecyclerView.Adapter<ConversationViewHo
     private int expandedItemPosition = -1;
     private int lastDeliveredPosition = -1;
     private int lastDisplayedPosition = -1;
-    private Observable<Long> timestampUpdateTimer;
+    private final Observable<Long> timestampUpdateTimer;
     private int lastMsgPos = -1;
 
     private boolean isComposing = false;
     private boolean mShowReadIndicator = true;
 
-    private static int[] msgBGLayouts = new int[] {
+    private static final int[] msgBGLayouts = new int[] {
             R.drawable.textmsg_bg_out_first,
             R.drawable.textmsg_bg_out_middle,
             R.drawable.textmsg_bg_out_last,
@@ -164,12 +166,31 @@ public class ConversationAdapter extends RecyclerView.Adapter<ConversationViewHo
         notifyDataSetChanged();
     }
 
-    public void add(Interaction e) {
-        boolean update = !mInteractions.isEmpty();
-        mInteractions.add(e);
-        notifyItemInserted(mInteractions.size() - 1);
-        if (update)
-            notifyItemChanged(mInteractions.size() - 2);
+    public boolean add(Interaction e) {
+        if (!TextUtils.isEmpty(e.getMessageId())) {
+            if (mInteractions.isEmpty() || e.getParentIds().contains(mInteractions.get(mInteractions.size()-1).getMessageId())) {
+                boolean update = !mInteractions.isEmpty();
+                mInteractions.add(e);
+                notifyItemInserted(mInteractions.size()-1);
+                if (update)
+                    notifyItemChanged(mInteractions.size()-2);
+                return true;
+            }
+            for (int i = 0, n = mInteractions.size(); i<n; i++) {
+                if (mInteractions.get(i).getParentIds().contains(e.getMessageId())) {
+                    mInteractions.add(i, e);
+                    notifyItemInserted(i);
+                    return i == n-1;
+                }
+            }
+        } else {
+            boolean update = !mInteractions.isEmpty();
+            mInteractions.add(e);
+            notifyItemInserted(mInteractions.size() - 1);
+            if (update)
+                notifyItemChanged(mInteractions.size() - 2);
+        }
+        return true;
     }
 
     public void update(Interaction e) {
@@ -253,16 +274,18 @@ public class ConversationAdapter extends RecyclerView.Adapter<ConversationViewHo
                         }
                     }
                     return out;
+                case INVALID:
+                    return MessageType.INVALID.ordinal();
             }
         }
-        return MessageType.CALL_INFORMATION.ordinal();
+        return -1;
     }
 
     @NonNull
     @Override
     public ConversationViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
         MessageType type = MessageType.values()[viewType];
-        ViewGroup v = (ViewGroup) LayoutInflater.from(parent.getContext()).inflate(type.layout, parent, false);
+        ViewGroup v = type == MessageType.INVALID ? new FrameLayout(parent.getContext()) : (ViewGroup) LayoutInflater.from(parent.getContext()).inflate(type.layout, parent, false);
         return new ConversationViewHolder(v, type);
     }
 
@@ -287,14 +310,20 @@ public class ConversationAdapter extends RecyclerView.Adapter<ConversationViewHo
             conversationViewHolder.itemView.startAnimation(animation);
         }
 
-        if (interaction.getType() == (InteractionType.TEXT)) {
-            configureForTextMessage(conversationViewHolder, interaction, position);
-        } else if (interaction.getType() == (InteractionType.CALL)) {
-            configureForCallInfo(conversationViewHolder, interaction);
-        } else if (interaction.getType() == (InteractionType.CONTACT)) {
-            configureForContactEvent(conversationViewHolder, interaction);
-        } else if (interaction.getType() == (InteractionType.DATA_TRANSFER)) {
-            configureForFileInfo(conversationViewHolder, interaction, position);
+        //Log.w(TAG, "onBindViewHolder " + interaction.getType() + " " + interaction);
+        if (interaction.getType() == InteractionType.INVALID) {
+            conversationViewHolder.itemView.setVisibility(View.GONE);
+        } else {
+            conversationViewHolder.itemView.setVisibility(View.VISIBLE);
+            if (interaction.getType() == InteractionType.TEXT) {
+                configureForTextMessage(conversationViewHolder, interaction, position);
+            } else if (interaction.getType() == InteractionType.CALL) {
+                configureForCallInfo(conversationViewHolder, interaction);
+            } else if (interaction.getType() == InteractionType.CONTACT) {
+                configureForContactEvent(conversationViewHolder, interaction);
+            } else if (interaction.getType() == InteractionType.DATA_TRANSFER) {
+                configureForFileInfo(conversationViewHolder, interaction, position);
+            }
         }
     }
 
@@ -584,7 +613,7 @@ public class ConversationAdapter extends RecyclerView.Adapter<ConversationViewHo
     private void configureForFileInfo(@NonNull final ConversationViewHolder viewHolder,
                                       @NonNull final Interaction interaction, int position) {
         DataTransfer file = (DataTransfer) interaction;
-        File path = presenter.getDeviceRuntimeService().getConversationPath(file.getPeerId(), file.getStoragePath());
+        File path = presenter.getDeviceRuntimeService().getConversationPath(interaction.getConversationId() == null ? interaction.getConversation().getParticipant() : interaction.getConversationId(), file.getStoragePath());
         if (file.isComplete())
             file.setSize(path.length());
 
@@ -697,28 +726,8 @@ public class ConversationAdapter extends RecyclerView.Adapter<ConversationViewHo
 
             if (file.getStatus() == InteractionStatus.TRANSFER_AWAITING_HOST) {
                 viewHolder.mAnswerLayout.setVisibility(View.VISIBLE);
-                viewHolder.btnAccept.setOnClickListener(v -> {
-                    if (!presenter.getDeviceRuntimeService().hasWriteExternalStoragePermission()) {
-                        conversationFragment.askWriteExternalStoragePermission();
-                        return;
-                    }
-                    Context context = v.getContext();
-                    File cacheDir = context.getCacheDir();
-                    long spaceLeft = AndroidFileUtils.getSpaceLeft(cacheDir.toString());
-                    if (spaceLeft == -1L || file.getTotalSize() > spaceLeft) {
-                        presenter.noSpaceLeft();
-                        return;
-                    }
-                    context.startService(new Intent(DRingService.ACTION_FILE_ACCEPT)
-                            .setClass(context.getApplicationContext(), DRingService.class)
-                            .putExtra(DRingService.KEY_TRANSFER_ID, file.getDaemonId()));
-                });
-                viewHolder.btnRefuse.setOnClickListener(v -> {
-                    Context context = v.getContext();
-                    context.startService(new Intent(DRingService.ACTION_FILE_CANCEL)
-                            .setClass(context.getApplicationContext(), DRingService.class)
-                            .putExtra(DRingService.KEY_TRANSFER_ID, file.getDaemonId()));
-                });
+                viewHolder.btnAccept.setOnClickListener(v -> presenter.acceptFile(file));
+                viewHolder.btnRefuse.setOnClickListener(v -> presenter.refuseFile(file));
             } else {
                 viewHolder.mAnswerLayout.setVisibility(View.GONE);
                 if (file.getStatus() == InteractionStatus.TRANSFER_ONGOING) {
@@ -769,9 +778,7 @@ public class ConversationAdapter extends RecyclerView.Adapter<ConversationViewHo
         }
         // Log.w(TAG, "configureForTextMessage " + position + " " + interaction.getDaemonId() + " " + interaction.getStatus());
 
-        convViewHolder.mCid = textMessage.getConversation().getParticipant();
         String message = textMessage.getBody().trim();
-
         View longPressView = convViewHolder.mMsgTxt;
         longPressView.getBackground().setTintList(null);
 
@@ -795,13 +802,13 @@ public class ConversationAdapter extends RecyclerView.Adapter<ConversationViewHo
             if (expandedItemPosition == position) {
                 expandedItemPosition = -1;
             }
-            conversationFragment.updatePosition(convViewHolder.getAdapterPosition());
+            conversationFragment.updatePosition(convViewHolder.getBindingAdapterPosition());
             if (textMessage.isIncoming()) {
                 longPressView.getBackground().setTint(conversationFragment.getResources().getColor(R.color.grey_500));
             } else {
                 longPressView.getBackground().setTint(conversationFragment.getResources().getColor(R.color.blue_900));
             }
-            mCurrentLongItem = new RecyclerViewContextMenuInfo(convViewHolder.getAdapterPosition(), v.getId());
+            mCurrentLongItem = new RecyclerViewContextMenuInfo(convViewHolder.getBindingAdapterPosition(), v.getId());
             return false;
         });
 
@@ -850,8 +857,10 @@ public class ConversationAdapter extends RecyclerView.Adapter<ConversationViewHo
                     });
                     convViewHolder.mAvatar.startAnimation(animation);
                 } else {
-                    convViewHolder.mAvatar.setImageBitmap(null);
-                    convViewHolder.mAvatar.setVisibility(View.INVISIBLE);
+                    if (convViewHolder.mAvatar != null) {
+                        convViewHolder.mAvatar.setImageBitmap(null);
+                        convViewHolder.mAvatar.setVisibility(View.INVISIBLE);
+                    }
                 }
             }
         } else {
@@ -938,16 +947,11 @@ public class ConversationAdapter extends RecyclerView.Adapter<ConversationViewHo
      */
     private void configureForCallInfo(@NonNull final ConversationViewHolder convViewHolder,
                                       @NonNull final Interaction interaction) {
-        int pictureResID;
-        String historyTxt;
         convViewHolder.mIcon.setScaleY(1);
         Context context = convViewHolder.itemView.getContext();
 
-
         View longPressView = convViewHolder.mCallInfoLayout;
         longPressView.getBackground().setTintList(null);
-
-
         longPressView.setOnCreateContextMenuListener((menu, v, menuInfo) -> {
             conversationFragment.onCreateContextMenu(menu, v, menuInfo);
             MenuInflater inflater = conversationFragment.getActivity().getMenuInflater();
@@ -958,7 +962,6 @@ public class ConversationAdapter extends RecyclerView.Adapter<ConversationViewHo
             menu.removeItem(R.id.conv_action_copy_text);
         });
 
-
         longPressView.setOnLongClickListener((View v) -> {
             longPressView.getBackground().setTint(conversationFragment.getResources().getColor(R.color.grey_500));
             conversationFragment.updatePosition(convViewHolder.getAdapterPosition());
@@ -966,9 +969,9 @@ public class ConversationAdapter extends RecyclerView.Adapter<ConversationViewHo
             return false;
         });
 
-
+        int pictureResID;
+        String historyTxt;
         SipCall call = (SipCall) interaction;
-
         if (call.isMissed()) {
             if (call.isIncoming()) {
                 pictureResID = R.drawable.baseline_call_missed_24;
@@ -989,7 +992,6 @@ public class ConversationAdapter extends RecyclerView.Adapter<ConversationViewHo
                     context.getString(R.string.notif_outgoing_call);
         }
 
-        convViewHolder.mCid = call.getConversation().getParticipant();
         convViewHolder.mIcon.setImageResource(pictureResID);
         convViewHolder.mHistTxt.setText(historyTxt);
         convViewHolder.mHistDetailTxt.setText(DateFormat.getDateTimeInstance()
@@ -1199,11 +1201,12 @@ public class ConversationAdapter extends RecyclerView.Adapter<ConversationViewHo
         CALL_INFORMATION(R.layout.item_conv_call),
         INCOMING_TEXT_MESSAGE(R.layout.item_conv_msg_peer),
         OUTGOING_TEXT_MESSAGE(R.layout.item_conv_msg_me),
-        COMPOSING_INDICATION(R.layout.item_conv_composing);
+        COMPOSING_INDICATION(R.layout.item_conv_composing),
+        INVALID(-1);
 
-        private final int layout;
+        @LayoutRes private final int layout;
 
-        MessageType(int l) {
+        MessageType(@LayoutRes int l) {
             layout = l;
         }
 
diff --git a/ring-android/app/src/main/java/cx/ring/adapters/NumberAdapter.java b/ring-android/app/src/main/java/cx/ring/adapters/NumberAdapter.java
index 2245fb92890c2dc5136db73c00b8caef67e4c6b1..5baba2cd3079a6d2cf1be9cb81ab700086e7326a 100644
--- a/ring-android/app/src/main/java/cx/ring/adapters/NumberAdapter.java
+++ b/ring-android/app/src/main/java/cx/ring/adapters/NumberAdapter.java
@@ -85,7 +85,7 @@ public class NumberAdapter extends BaseAdapter {
 
         Phone number = mNumbers.get(position);
         ImageView numberIcon = convertView.findViewById(R.id.number_icon);
-        numberIcon.setImageResource(number.getNumber().isRingId() ?
+        numberIcon.setImageResource(number.getNumber().isHexId() ?
                 R.drawable.ic_jami_24 : R.drawable.baseline_dialer_sip_24);
 
         if (longView) {
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 17bc1fc326fc86a370230717a6da4e4f2da6d8c7..9113f74e5d279c0cc2843afa954dd685094a09a5 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
@@ -26,6 +26,7 @@ import cx.ring.smartlist.SmartListViewModel;
 import cx.ring.viewholders.SmartListViewHolder;
 
 import android.os.Parcelable;
+import android.util.Log;
 import android.view.LayoutInflater;
 import android.view.ViewGroup;
 
@@ -90,6 +91,7 @@ public class SmartListAdapter extends RecyclerView.Adapter<SmartListViewHolder>
     }
 
     public void update(List<SmartListViewModel> viewModels) {
+        //Log.w("SmartListAdapter", "update " + (viewModels == null ? null : viewModels.size()));
         final List<SmartListViewModel> old = mSmartListViewModels;
         mSmartListViewModels = viewModels == null ? new ArrayList<>() : viewModels;
         if (old != null && viewModels != null) {
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 e30910a7c58c2eb26ea77a108566a23e4f511606..7f0c67db62fa974995ce981f38cd3a447d7ee858 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
@@ -27,8 +27,8 @@ import cx.ring.smartlist.SmartListViewModel;
 
 public class SmartListDiffUtil extends DiffUtil.Callback {
 
-    private List<SmartListViewModel> mOldList;
-    private List<SmartListViewModel> mNewList;
+    private final List<SmartListViewModel> mOldList;
+    private final List<SmartListViewModel> mNewList;
 
     public SmartListDiffUtil(List<SmartListViewModel> oldList, List<SmartListViewModel> newList) {
         mOldList = oldList;
@@ -49,7 +49,17 @@ public class SmartListDiffUtil extends DiffUtil.Callback {
     public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
         SmartListViewModel oldItem = mOldList.get(oldItemPosition);
         SmartListViewModel newItem = mNewList.get(newItemPosition);
-        return newItem.getHeaderTitle() == oldItem.getHeaderTitle() && newItem.getContact() == oldItem.getContact();
+        if (newItem.getHeaderTitle() != oldItem.getHeaderTitle())
+            return false;
+        if (newItem.getContact() != oldItem.getContact()) {
+            if (newItem.getContact().size() != oldItem.getContact().size())
+                return false;
+            for (int i = 0; i < newItem.getContact().size(); i++) {
+                if (newItem.getContact().get(i) != oldItem.getContact().get(i))
+                    return false;
+            }
+        }
+        return true;
     }
 
     @Override
diff --git a/ring-android/app/src/main/java/cx/ring/application/JamiApplication.java b/ring-android/app/src/main/java/cx/ring/application/JamiApplication.java
index f45152ee8f39d27e00f8fbc37c16daf2287b37dc..c867059aac1209d9b33ecfb33d2e83333994c558 100644
--- a/ring-android/app/src/main/java/cx/ring/application/JamiApplication.java
+++ b/ring-android/app/src/main/java/cx/ring/application/JamiApplication.java
@@ -52,6 +52,8 @@ import java.util.concurrent.ScheduledExecutorService;
 import javax.inject.Inject;
 import javax.inject.Named;
 
+import androidx.annotation.RequiresApi;
+
 import cx.ring.BuildConfig;
 import cx.ring.R;
 import cx.ring.contacts.AvatarFactory;
@@ -238,7 +240,7 @@ public abstract class JamiApplication extends Application {
         super.onCreate();
         sInstance = this;
 
-        RxJavaPlugins.setErrorHandler(e -> Log.e(TAG, "Unhandled RxJava error", e));
+        //RxJavaPlugins.setErrorHandler(e -> Log.e(TAG, "Unhandled RxJava error", e));
 
         // building injection dependency tree
         mJamiInjectionComponent = DaggerJamiInjectionComponent.builder()
diff --git a/ring-android/app/src/main/java/cx/ring/client/CallActivity.java b/ring-android/app/src/main/java/cx/ring/client/CallActivity.java
index 40a5341c0c8301737f14adce731b47b1000f685a..4c94edc11f413afb30cbee439a59e778f309f363 100644
--- a/ring-android/app/src/main/java/cx/ring/client/CallActivity.java
+++ b/ring-android/app/src/main/java/cx/ring/client/CallActivity.java
@@ -42,8 +42,8 @@ import cx.ring.BuildConfig;
 import cx.ring.R;
 import cx.ring.application.JamiApplication;
 import cx.ring.fragments.CallFragment;
-import cx.ring.fragments.ConversationFragment;
 import cx.ring.services.NotificationService;
+import cx.ring.utils.ConversationPath;
 import cx.ring.utils.KeyboardVisibilityManager;
 import cx.ring.utils.MediaButtonsHelper;
 
@@ -118,22 +118,16 @@ public class CallActivity extends AppCompatActivity {
 
     private void handleNewIntent(Intent intent) {
         String action = intent.getAction();
-
         if (Intent.ACTION_CALL.equals(action) || ACTION_CALL.equals(action)) {
-
             boolean audioOnly = intent.getBooleanExtra(CallFragment.KEY_AUDIO_ONLY, true);
-            String accountId = intent.getStringExtra(ConversationFragment.KEY_ACCOUNT_ID);
-            String contactRingId = intent.getStringExtra(ConversationFragment.KEY_CONTACT_RING_ID);
-            // Reload a new view
+            String contactId = intent.getStringExtra(Intent.EXTRA_PHONE_NUMBER);
             CallFragment callFragment = CallFragment.newInstance(CallFragment.ACTION_PLACE_CALL,
-                    accountId,
-                    contactRingId,
+                    ConversationPath.fromIntent(intent),
+                    contactId,
                     audioOnly);
             getSupportFragmentManager().beginTransaction().replace(R.id.main_call_layout, callFragment, CALL_FRAGMENT_TAG).commit();
-
         } else if (Intent.ACTION_VIEW.equals(action) || ACTION_CALL_ACCEPT.equals(action)) {
             String confId = intent.getStringExtra(NotificationService.KEY_CALL_ID);
-            // Reload a new view
             CallFragment callFragment = CallFragment.newInstance(Intent.ACTION_VIEW.equals(action) ? CallFragment.ACTION_GET_CALL : ACTION_CALL_ACCEPT, confId);
             getSupportFragmentManager().beginTransaction().replace(R.id.main_call_layout, callFragment, CALL_FRAGMENT_TAG).commit();
         }
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 92339a9222e052fd5d88f8b01480be5b0b674331..ca63edba7caf5decef6c62c867c2b9d0b5dee39a 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
@@ -33,7 +33,9 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder;
 import com.google.android.material.floatingactionbutton.FloatingActionButton;
 import com.google.android.material.snackbar.Snackbar;
 
+import androidx.annotation.DrawableRes;
 import androidx.annotation.NonNull;
+import androidx.annotation.StringRes;
 import androidx.appcompat.app.AppCompatActivity;
 import androidx.appcompat.widget.Toolbar;
 
@@ -43,15 +45,18 @@ import android.view.ViewGroup;
 import android.widget.Toast;
 
 import java.util.ArrayList;
+import java.util.Map;
 
 import javax.inject.Inject;
 import javax.inject.Singleton;
 
 import androidx.core.widget.ImageViewCompat;
-import androidx.databinding.DataBindingUtil;
 import androidx.recyclerview.widget.RecyclerView;
+
 import cx.ring.R;
 import cx.ring.application.JamiApplication;
+import cx.ring.daemon.Ringservice;
+import cx.ring.daemon.RingserviceJNI;
 import cx.ring.databinding.ActivityContactDetailsBinding;
 import cx.ring.databinding.ItemContactActionBinding;
 import cx.ring.facades.ConversationFacade;
@@ -61,6 +66,7 @@ import cx.ring.model.CallContact;
 import cx.ring.model.Conference;
 import cx.ring.model.Conversation;
 import cx.ring.model.SipCall;
+import cx.ring.model.Uri;
 import cx.ring.services.AccountService;
 import cx.ring.services.NotificationService;
 import cx.ring.utils.ConversationPath;
@@ -82,25 +88,27 @@ public class ContactDetailsActivity extends AppCompatActivity {
     private SharedPreferences mPreferences;
     private ActivityContactDetailsBinding binding;
     private Conversation mConversation;
-    private CallContact mContact = null;
 
     interface IContactAction {
         void onAction();
     }
 
     static class ContactAction {
+        @DrawableRes
         final int icon;
-        int iconTint;
-        CharSequence title;
+        final CharSequence title;
         final IContactAction callback;
 
-        ContactAction(int i, int tint, CharSequence t, IContactAction cb) {
+        int iconTint;
+
+        ContactAction(@DrawableRes int i, int tint, CharSequence t, IContactAction cb) {
             icon = i;
             iconTint = tint;
             title = t;
             callback = cb;
         }
-        ContactAction(int i, CharSequence t, IContactAction cb) {
+
+        ContactAction(@DrawableRes int i, CharSequence t, IContactAction cb) {
             icon = i;
             iconTint = Color.BLACK;
             title = t;
@@ -110,12 +118,12 @@ public class ContactDetailsActivity extends AppCompatActivity {
         void setIconTint(int tint) {
             iconTint = tint;
         }
-        void setTitle(CharSequence t) { title = t; }
     }
 
     static class ContactActionView extends RecyclerView.ViewHolder {
         final ItemContactActionBinding binding;
         IContactAction callback;
+
         ContactActionView(@NonNull ItemContactActionBinding b) {
             super(b.getRoot());
             binding = b;
@@ -124,7 +132,7 @@ public class ContactDetailsActivity extends AppCompatActivity {
                     if (callback != null)
                         callback.onAction();
                 } catch (Exception e) {
-                    Log.w(TAG, "Error performing action" ,e);
+                    Log.w(TAG, "Error performing action", e);
                 }
             });
         }
@@ -163,11 +171,15 @@ public class ContactDetailsActivity extends AppCompatActivity {
     private ContactAction colorAction;
     private ContactAction contactAction;
     private int colorActionPosition;
-    private int contactIdPosition;
 
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
+        ConversationPath path = ConversationPath.fromIntent(getIntent());
+        if (path == null) {
+            finish();
+            return;
+        }
         binding = ActivityContactDetailsBinding.inflate(getLayoutInflater());
         setContentView(binding.getRoot());
         JamiApplication.getInstance().getInjectionComponent().inject(this);
@@ -175,95 +187,99 @@ public class ContactDetailsActivity extends AppCompatActivity {
         CollapsingToolbarLayout collapsingToolbarLayout = findViewById(R.id.toolbar_layout);
         collapsingToolbarLayout.setTitle("");
 
-        Toolbar toolbar = findViewById(R.id.toolbar);
-        setSupportActionBar(toolbar);
+        setSupportActionBar(findViewById(R.id.toolbar));
 
         FloatingActionButton fab = findViewById(R.id.fab);
-        fab.setOnClickListener(view -> goToConversationActivity(mConversation.getAccountId(), mContact.getPrimaryNumber()));
+        fab.setOnClickListener(view -> goToConversationActivity(mConversation.getAccountId(), mConversation.getUri()));
 
-        Intent intent = getIntent();
-        ConversationPath path = ConversationPath.fromIntent(intent);
-        if (path != null) {
-            mDisposableBag.add(mConversationFacade
+        colorActionPosition = 1;
+
+        mDisposableBag.add(mConversationFacade
                 .startConversation(path.getAccountId(), path.getConversationUri())
                 .observeOn(AndroidSchedulers.mainThread())
                 .subscribe(conversation -> {
-                    // TODO handle group
-                    CallContact contact = conversation.getContact();
+                    mConversation = conversation;
                     mPreferences = getSharedPreferences(conversation.getAccountId() + "_" + conversation.getUri().getUri(), Context.MODE_PRIVATE);
-                    int color = mPreferences.getInt(ConversationFragment.KEY_PREFERENCE_CONVERSATION_COLOR, getResources().getColor(R.color.color_primary_light));
-                    colorAction.setIconTint(color);
-                    adapter.notifyItemChanged(colorActionPosition);
-                    contactAction.setTitle(contact.getRingUsername());
-                    adapter.notifyItemChanged(contactIdPosition);
-                    collapsingToolbarLayout.setBackgroundColor(color);
-                    collapsingToolbarLayout.setTitle(contact.getDisplayName());
-                    collapsingToolbarLayout.setContentScrimColor(color);
-                    collapsingToolbarLayout.setStatusBarScrimColor(color);
-                    //collapsingToolbarLayout.setCollapsedTitleTextColor();
                     binding.contactImage.setImageDrawable(
                             new AvatarDrawable.Builder()
-                                    .withContact(contact)
+                                    .withConversation(conversation)
                                     .withPresence(false)
                                     .withCircleCrop(false)
                                     .build(this)
                     );
-                    mConversation = conversation;
-                    mContact = contact;
-                }));
 
-            colorAction = new ContactAction(R.drawable.item_color_background, 0, "Choose color", () -> {
-                ColorChooserBottomSheet frag = new ColorChooserBottomSheet();
-                frag.setCallback(color -> {
+                    /*Map<String, String> details = Ringservice.getCertificateDetails(conversation.getContact().getUri().getRawRingId());
+                    for (Map.Entry<String, String> e : details.entrySet()) {
+                        Log.w(TAG, e.getKey() + " -> " + e.getValue());
+                    }*/
+
+                    @StringRes int infoString = conversation.isSwarm()
+                            ? (conversation.getMode() == Conversation.Mode.OneToOne
+                                ? 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), () -> {}));
+
+                    colorAction = new ContactAction(R.drawable.item_color_background, 0, getText(R.string.conversation_preference_color), () -> {
+                        ColorChooserBottomSheet frag = new ColorChooserBottomSheet();
+                        frag.setCallback(color -> {
+                            collapsingToolbarLayout.setBackgroundColor(color);
+                            collapsingToolbarLayout.setContentScrimColor(color);
+                            collapsingToolbarLayout.setStatusBarScrimColor(color);
+                            colorAction.setIconTint(color);
+                            adapter.notifyItemChanged(colorActionPosition);
+                            mPreferences.edit().putInt(ConversationFragment.KEY_PREFERENCE_CONVERSATION_COLOR, color).apply();
+                        });
+                        frag.show(getSupportFragmentManager(), "colorChooser");
+                    });
+                    int color = mPreferences.getInt(ConversationFragment.KEY_PREFERENCE_CONVERSATION_COLOR, getResources().getColor(R.color.color_primary_light));
+                    colorAction.setIconTint(color);
                     collapsingToolbarLayout.setBackgroundColor(color);
+                    collapsingToolbarLayout.setTitle(conversation.getTitle());
                     collapsingToolbarLayout.setContentScrimColor(color);
                     collapsingToolbarLayout.setStatusBarScrimColor(color);
-                    colorAction.setIconTint(color);
-                    adapter.notifyItemChanged(colorActionPosition);
-                    mPreferences.edit().putInt(ConversationFragment.KEY_PREFERENCE_CONVERSATION_COLOR, color).apply();
-                });
-                frag.show(getSupportFragmentManager(), "colorChooser");
-            });
-            adapter.actions.add(colorAction);
-            adapter.actions.add(new ContactAction(R.drawable.baseline_call_24, getText(R.string.ab_action_audio_call), () ->
-                    goToCallActivity(mConversation.getAccountId(), mContact.getPrimaryNumber(), true)));
-            adapter.actions.add(new ContactAction(R.drawable.baseline_videocam_24, getText(R.string.ab_action_video_call), () ->
-                    goToCallActivity(mConversation.getAccountId(), mContact.getPrimaryNumber(), false)));
-            adapter.actions.add(new ContactAction(R.drawable.baseline_clear_all_24, getText(R.string.conversation_action_history_clear), () ->
-                    new MaterialAlertDialogBuilder(ContactDetailsActivity.this)
-                            .setTitle(R.string.clear_history_dialog_title)
-                            .setMessage(R.string.clear_history_dialog_message)
-                            .setPositiveButton(R.string.conversation_action_history_clear, (b, i) -> {
-                                mConversationFacade.clearHistory(mConversation.getAccountId(), mContact.getPrimaryUri()).subscribe();
-                                Snackbar.make(binding.getRoot(), R.string.clear_history_completed, Snackbar.LENGTH_LONG).show();
-                            })
-                            .setNegativeButton(android.R.string.cancel, null)
-                            .create()
-                            .show()));
-            adapter.actions.add(new ContactAction(R.drawable.baseline_block_24, getText(R.string.conversation_action_block_this), () ->
-                    new MaterialAlertDialogBuilder(ContactDetailsActivity.this)
-                            .setTitle(getString(R.string.block_contact_dialog_title, contactAction.title))
-                            .setMessage(getString(R.string.block_contact_dialog_message, contactAction.title))
-                            .setPositiveButton(R.string.conversation_action_block_this, (b, i) -> {
-                                mAccountService.removeContact(mConversation.getAccountId(), mContact.getPrimaryUri().getRawRingId(), true);
-                                Toast.makeText(getApplicationContext(), getString(R.string.block_contact_completed, contactAction.title), Toast.LENGTH_LONG).show();
-                                finish();
-                            })
-                            .setNegativeButton(android.R.string.cancel, null)
-                            .create()
-                            .show()));
-            contactAction = new ContactAction(R.drawable.baseline_person_24, "", () -> {
-                ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
-                if (clipboard != null) {
-                    clipboard.setPrimaryClip(ClipData.newPlainText(getText(R.string.clip_contact_uri), path.getConversationId()));
-                    Snackbar.make(binding.getRoot(), getString(R.string.conversation_action_copied_peer_number_clipboard, path.getConversationId()), Snackbar.LENGTH_LONG).show();
-                }
-            });
-            adapter.actions.add(contactAction);
-            colorActionPosition = 0;
-            contactIdPosition = adapter.actions.size() - 1;
-            binding.contactActionList.setAdapter(adapter);
-        }
+                    adapter.actions.add(colorAction);
+
+                    if (mConversation.getContacts().size() <= 2) {
+                        CallContact contact = mConversation.getContact();
+                        adapter.actions.add(new ContactAction(R.drawable.baseline_call_24, getText(R.string.ab_action_audio_call), () ->
+                                goToCallActivity(mConversation.getAccountId(), contact.getPrimaryNumber(), true)));
+                        adapter.actions.add(new ContactAction(R.drawable.baseline_videocam_24, getText(R.string.ab_action_video_call), () ->
+                                goToCallActivity(mConversation.getAccountId(), contact.getPrimaryNumber(), false)));
+                        adapter.actions.add(new ContactAction(R.drawable.baseline_clear_all_24, getText(R.string.conversation_action_history_clear), () ->
+                                new MaterialAlertDialogBuilder(ContactDetailsActivity.this)
+                                        .setTitle(R.string.clear_history_dialog_title)
+                                        .setMessage(R.string.clear_history_dialog_message)
+                                        .setPositiveButton(R.string.conversation_action_history_clear, (b, i) -> {
+                                            mConversationFacade.clearHistory(mConversation.getAccountId(), contact.getUri()).subscribe();
+                                            Snackbar.make(binding.getRoot(), R.string.clear_history_completed, Snackbar.LENGTH_LONG).show();
+                                        })
+                                        .setNegativeButton(android.R.string.cancel, null)
+                                        .create()
+                                        .show()));
+                        adapter.actions.add(new ContactAction(R.drawable.baseline_block_24, getText(R.string.conversation_action_block_this), () ->
+                                new MaterialAlertDialogBuilder(ContactDetailsActivity.this)
+                                        .setTitle(getString(R.string.block_contact_dialog_title, contactAction.title))
+                                        .setMessage(getString(R.string.block_contact_dialog_message, contactAction.title))
+                                        .setPositiveButton(R.string.conversation_action_block_this, (b, i) -> {
+                                            mAccountService.removeContact(mConversation.getAccountId(), contact.getUri().getRawRingId(), true);
+                                            Toast.makeText(getApplicationContext(), getString(R.string.block_contact_completed, contactAction.title), Toast.LENGTH_LONG).show();
+                                            finish();
+                                        })
+                                        .setNegativeButton(android.R.string.cancel, null)
+                                        .create()
+                                        .show()));
+                    }
+                    contactAction = new ContactAction(R.drawable.baseline_person_24, conversation.getUriTitle(), () -> {
+                        ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
+                        if (clipboard != null) {
+                            clipboard.setPrimaryClip(ClipData.newPlainText(getText(R.string.clip_contact_uri), path.getConversationId()));
+                            Snackbar.make(binding.getRoot(), getString(R.string.conversation_action_copied_peer_number_clipboard, path.getConversationId()), Snackbar.LENGTH_LONG).show();
+                        }
+                    });
+                    adapter.actions.add(contactAction);
+                    binding.contactActionList.setAdapter(adapter);
+                }));
     }
 
     @Override
@@ -279,7 +295,6 @@ public class ContactDetailsActivity extends AppCompatActivity {
 
     private void goToCallActivity(String accountId, String contactRingId, boolean audioOnly) {
         Conference conf = mConversation.getCurrentCall();
-
         if (conf != null
                 && !conf.getParticipants().isEmpty()
                 && conf.getParticipants().get(0).getCallStatus() != SipCall.CallStatus.INACTIVE
@@ -297,7 +312,7 @@ public class ContactDetailsActivity extends AppCompatActivity {
         }
     }
 
-    private void goToConversationActivity(String accountId, String contactRingId) {
-        startActivity(new Intent(Intent.ACTION_VIEW, ConversationPath.toUri(accountId, contactRingId), getApplicationContext(), ConversationActivity.class));
+    private void goToConversationActivity(String accountId, Uri conversationUri) {
+        startActivity(new Intent(Intent.ACTION_VIEW, ConversationPath.toUri(accountId, conversationUri), getApplicationContext(), ConversationActivity.class));
     }
 }
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 2f82a3acfd41797fa6de0a43b6433c812bd81f3d..8f3a1948fbcec5de80b5b9c42f0c7a48ca2686b2 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
@@ -77,22 +77,6 @@ public class ConversationActivity extends AppCompatActivity implements Colorable
         if (ab != null)
             ab.setDisplayHomeAsUpEnabled(true);
 
-        //getWindow().setFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS, WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS);
-        View decorView = getWindow().getDecorView();
-        decorView.setSystemUiVisibility(
-                decorView.getSystemUiVisibility()
-                        | View.SYSTEM_UI_FLAG_LAYOUT_STABLE
-                        | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
-                        | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);
-
-        ViewCompat.setOnApplyWindowInsetsListener(binding.toolbarLayout, (v, insets) -> {
-            CoordinatorLayout.LayoutParams params = (CoordinatorLayout.LayoutParams) binding.toolbarLayout.getLayoutParams();
-            params.topMargin = insets.getSystemWindowInsetTop();
-            binding.toolbarLayout.setLayoutParams(params);
-            insets.consumeSystemWindowInsets();
-            return insets;
-        });
-
         if (mConversationFragment == null) {
             Bundle bundle = conversationPath.toBundle();
             bundle.putBoolean(NotificationServiceImpl.EXTRA_BUBBLE, mIsBubble);
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 26305f190ce13c9e1a1d778bc0f90e97fc78e801..36bbcac66f2c42a9861d10bf9f6c2e8479431b1b 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
@@ -39,6 +39,8 @@ import cx.ring.adapters.SmartListAdapter;
 import cx.ring.application.JamiApplication;
 import cx.ring.facades.ConversationFacade;
 import cx.ring.fragments.CallFragment;
+import cx.ring.model.Account;
+import cx.ring.model.CallContact;
 import cx.ring.model.Conference;
 import cx.ring.model.SipCall;
 import cx.ring.services.CallService;
@@ -80,7 +82,7 @@ public class ConversationSelectionActivity extends AppCompatActivity {
             @Override
             public void onItemClick(SmartListViewModel smartListViewModel) {
                 Intent intent = new Intent();
-                intent.setData(ConversationPath.toUri(smartListViewModel.getAccountId(), smartListViewModel.getContact().getPrimaryNumber()));
+                intent.setData(ConversationPath.toUri(smartListViewModel.getAccountId(), smartListViewModel.getUri()));
                 setResult(Activity.RESULT_OK, intent);
                 finish();
             }
@@ -115,8 +117,11 @@ public class ConversationSelectionActivity extends AppCompatActivity {
                         return vm;
                     List<SmartListViewModel> filteredVms = new ArrayList<>(vm.size());
                     models: for (SmartListViewModel v : vm) {
+                        List<CallContact> contacts = v.getContact();
+                        if (contacts.size() != 1)
+                            continue;
                         for (SipCall call : conf.getParticipants()) {
-                            if (call.getContact() == v.getContact()) {
+                            if (call.getContact() == v.getContact().get(0)) {
                                 continue models;
                             }
                         }
diff --git a/ring-android/app/src/main/java/cx/ring/client/HomeActivity.java b/ring-android/app/src/main/java/cx/ring/client/HomeActivity.java
index 78bcd21b6a322b24b6834def1e57c6f1d519b8ca..03ea2b0c2986026dd18897ffae203185e02fd1e4 100644
--- a/ring-android/app/src/main/java/cx/ring/client/HomeActivity.java
+++ b/ring-android/app/src/main/java/cx/ring/client/HomeActivity.java
@@ -389,13 +389,14 @@ public class HomeActivity extends AppCompatActivity implements BottomNavigationV
         mDisposable.add(mAccountService.getCurrentAccountSubject()
                 .firstElement()
                 .observeOn(AndroidSchedulers.mainThread())
-                .subscribe(account -> startConversation(account.getAccountID(), new cx.ring.model.Uri(conversationId))));
+                .subscribe(account -> startConversation(account.getAccountID(), cx.ring.model.Uri.fromString(conversationId))));
     }
     public void startConversation(String accountId, cx.ring.model.Uri conversationId) {
+        Log.w(TAG, "startConversation " + accountId + " " + conversationId);
         if (!DeviceUtils.isTablet(this)) {
-            startActivity(new Intent(Intent.ACTION_VIEW, ConversationPath.toUri(accountId, conversationId.toString()), this, ConversationActivity.class));
+            startActivity(new Intent(Intent.ACTION_VIEW, ConversationPath.toUri(accountId, conversationId), this, ConversationActivity.class));
         } else {
-            startConversationTablet(ConversationPath.toBundle(accountId, conversationId.toString()));
+            startConversationTablet(ConversationPath.toBundle(accountId, conversationId));
         }
     }
 
@@ -510,13 +511,11 @@ public class HomeActivity extends AppCompatActivity implements BottomNavigationV
         switch (item.getItemId()) {
             case R.id.navigation_requests:
                 if (fContent instanceof ContactRequestsFragment) {
-                    ((ContactRequestsFragment) fContent).presentForAccount(account.getAccountID());
+                    ((ContactRequestsFragment) fContent).presentForAccount(null);
                     break;
                 }
                 popCustomBackStack();
                 fContent = new ContactRequestsFragment();
-                bundle.putString(ContactRequestsFragment.ACCOUNT_ID, account.getAccountID());
-                fContent.setArguments(bundle);
                 getSupportFragmentManager().beginTransaction()
                         .setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE)
                         .replace(R.id.main_frame, fContent, CONTACT_REQUESTS_TAG)
diff --git a/ring-android/app/src/main/java/cx/ring/contactrequests/ContactRequestsFragment.java b/ring-android/app/src/main/java/cx/ring/contactrequests/ContactRequestsFragment.java
index 09ea6b507baf76e8d1892532ca7e1e2c63ec75a8..96d7415cae6a45f420708541b59fb79074f0cf06 100644
--- a/ring-android/app/src/main/java/cx/ring/contactrequests/ContactRequestsFragment.java
+++ b/ring-android/app/src/main/java/cx/ring/contactrequests/ContactRequestsFragment.java
@@ -41,6 +41,7 @@ import cx.ring.application.JamiApplication;
 import cx.ring.client.ConversationActivity;
 import cx.ring.client.HomeActivity;
 import cx.ring.databinding.FragPendingContactRequestsBinding;
+import cx.ring.model.Uri;
 import cx.ring.mvp.BaseSupportFragment;
 import cx.ring.smartlist.SmartListViewModel;
 import cx.ring.utils.ConversationPath;
@@ -74,7 +75,7 @@ public class ContactRequestsFragment extends BaseSupportFragment<ContactRequests
         binding = null;
     }
 
-    public void presentForAccount(@NonNull String accountId) {
+    public void presentForAccount(@Nullable String accountId) {
         Bundle arguments = getArguments();
         if (arguments != null)
             arguments.putString(ACCOUNT_ID, accountId);
@@ -86,9 +87,8 @@ public class ContactRequestsFragment extends BaseSupportFragment<ContactRequests
     public void onStart() {
         super.onStart();
         Bundle arguments = getArguments();
-        if (arguments != null && arguments.containsKey(ACCOUNT_ID)) {
-            presenter.updateAccount(getArguments().getString(ACCOUNT_ID));
-        }
+        String accountId = arguments != null ? arguments.getString(ACCOUNT_ID) : null;
+        presenter.updateAccount(accountId);
     }
 
     @Override
@@ -145,21 +145,13 @@ public class ContactRequestsFragment extends BaseSupportFragment<ContactRequests
     }
 
     @Override
-    public void goToConversation(String accountId, String contactId) {
-        Context context = requireContext();
-        if (DeviceUtils.isTablet(context)) {
-            Activity activity = getActivity();
-            if (activity instanceof HomeActivity) {
-                ((HomeActivity) activity).startConversationTablet(ConversationPath.toBundle(accountId, contactId));
-            }
-        } else {
-            startActivity(new Intent(Intent.ACTION_VIEW, ConversationPath.toUri(accountId, contactId), context, ConversationActivity.class));
-        }
+    public void goToConversation(String accountId, Uri contactId) {
+        ((HomeActivity) requireActivity()).startConversation(accountId, contactId);
     }
 
     @Override
     public void onItemClick(SmartListViewModel viewModel) {
-        presenter.contactRequestClicked(viewModel.getAccountId(), viewModel.getContact());
+        presenter.contactRequestClicked(viewModel.getAccountId(), viewModel.getUri());
     }
 
     @Override
diff --git a/ring-android/app/src/main/java/cx/ring/contacts/AvatarFactory.java b/ring-android/app/src/main/java/cx/ring/contacts/AvatarFactory.java
index 2288b08bbda2750b91f7ff13f6ffbfc1858e9cd7..f68a032c47496f5cff219ab4d7ff698e7eb28f39 100644
--- a/ring-android/app/src/main/java/cx/ring/contacts/AvatarFactory.java
+++ b/ring-android/app/src/main/java/cx/ring/contacts/AvatarFactory.java
@@ -36,6 +36,8 @@ import java.util.List;
 
 import cx.ring.model.Account;
 import cx.ring.model.CallContact;
+import cx.ring.model.Conversation;
+import cx.ring.smartlist.SmartListViewModel;
 import cx.ring.utils.BitmapUtils;
 import cx.ring.views.AvatarDrawable;
 import io.reactivex.Single;
@@ -60,23 +62,26 @@ public class AvatarFactory {
                         .withPresence(presence)
                         .build(context));
     }
-    public static Single<Drawable> getAvatar(Context context, List<CallContact> contacts, boolean presence) {
+    public static Single<Drawable> getAvatar(Context context, Conversation conversation, boolean presence) {
         return Single.fromCallable(() ->
                 new AvatarDrawable.Builder()
-                        .withContacts(contacts)
+                        .withConversation(conversation)
                         .withCircleCrop(true)
                         .withPresence(presence)
                         .build(context));
     }
+    public static Single<Drawable> getAvatar(Context context, SmartListViewModel vm) {
+        return Single.fromCallable(() ->
+                new AvatarDrawable.Builder()
+                        .withViewModel(vm)
+                        .withCircleCrop(true)
+                        .build(context));
+    }
     public static Single<Drawable> getAvatar(Context context, CallContact contact) {
         return getAvatar(context, contact, true);
     }
-    public static Single<Drawable> getAvatar(Context context, List<CallContact> contacts) {
-        return getAvatar(context, contacts, true);
-    }
-
-    public static Single<Bitmap> getBitmapAvatar(Context context, List<CallContact> contacts, int size, boolean presence) {
-        return getAvatar(context, contacts, presence)
+    public static Single<Bitmap> getBitmapAvatar(Context context, Conversation conversation, int size, boolean presence) {
+        return getAvatar(context, conversation, presence)
                 .map(d -> BitmapUtils.drawableToBitmap(d, size));
     }
     public static Single<Bitmap> getBitmapAvatar(Context context, CallContact contact, int size, boolean presence) {
diff --git a/ring-android/app/src/main/java/cx/ring/dependencyinjection/JamiInjectionComponent.java b/ring-android/app/src/main/java/cx/ring/dependencyinjection/JamiInjectionComponent.java
index 32e31932b7644683cb026b8bc03f290148e13085..05d74bcf4202a69af5a3c6a237555fa886839c3b 100755
--- a/ring-android/app/src/main/java/cx/ring/dependencyinjection/JamiInjectionComponent.java
+++ b/ring-android/app/src/main/java/cx/ring/dependencyinjection/JamiInjectionComponent.java
@@ -28,6 +28,7 @@ import cx.ring.account.JamiLinkAccountPasswordFragment;
 import cx.ring.contactrequests.BlockListFragment;
 import cx.ring.fragments.GeneralAccountFragment;
 import cx.ring.fragments.LinkDeviceFragment;
+import cx.ring.fragments.ContactPickerFragment;
 import cx.ring.fragments.LocationSharingFragment;
 import cx.ring.account.AccountWizardActivity;
 import cx.ring.account.HomeAccountCreationFragment;
@@ -234,4 +235,6 @@ public interface JamiInjectionComponent {
     void inject(SyncService syncService);
 
     void inject(LinkDeviceFragment linkDeviceFragment);
+
+    void inject(ContactPickerFragment contactPickerFragment);
 }
diff --git a/ring-android/app/src/main/java/cx/ring/dependencyinjection/JamiInjectionModule.java b/ring-android/app/src/main/java/cx/ring/dependencyinjection/JamiInjectionModule.java
index 799304c123869b069004194695c0e77ded79d177..15dbe6703e4828e7db53c8bd7e80075e3add631a 100755
--- a/ring-android/app/src/main/java/cx/ring/dependencyinjection/JamiInjectionModule.java
+++ b/ring-android/app/src/main/java/cx/ring/dependencyinjection/JamiInjectionModule.java
@@ -37,7 +37,7 @@ public class JamiInjectionModule {
     }
 
     @Provides
-    JamiApplication provideRingApplication() {
+    JamiApplication provideJamiApplication() {
         return mJamiApplication;
     }
 
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 15b0b8b5a1e03d24f0d53a292c976f38252ba599..c6babb4f9c3c89f34386843e0d9e7f968fb8845f 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
@@ -190,11 +190,12 @@ public class CallFragment extends BaseSupportFragment<CallPresenter> implements
 
     private final CompositeDisposable mCompositeDisposable = new CompositeDisposable();
 
-    public static CallFragment newInstance(@NonNull String action, @Nullable String accountID, @Nullable String contactRingId, boolean audioOnly) {
+    public static CallFragment newInstance(@NonNull String action, @Nullable ConversationPath path, @Nullable String contactId, boolean audioOnly) {
         Bundle bundle = new Bundle();
         bundle.putString(KEY_ACTION, action);
-        bundle.putString(KEY_ACCOUNT_ID, accountID);
-        bundle.putString(ConversationFragment.KEY_CONTACT_RING_ID, contactRingId);
+        if (path != null)
+            path.toBundle(bundle);
+        bundle.putString(Intent.EXTRA_PHONE_NUMBER, contactId);
         bundle.putBoolean(KEY_AUDIO_ONLY, audioOnly);
         CallFragment countDownFragment = new CallFragment();
         countDownFragment.setArguments(bundle);
@@ -240,7 +241,6 @@ public class CallFragment extends BaseSupportFragment<CallPresenter> implements
 
     @Override
     protected void initPresenter(CallPresenter presenter) {
-        super.initPresenter(presenter);
         Bundle args = getArguments();
         if (args != null) {
             String action = args.getString(KEY_ACTION);
@@ -808,7 +808,7 @@ public class CallFragment extends BaseSupportFragment<CallPresenter> implements
             if (resultCode == Activity.RESULT_OK && data != null) {
                 ConversationPath path = ConversationPath.fromUri(data.getData());
                 if (path != null) {
-                    presenter.addConferenceParticipant(path.getAccountId(), path.getConversationId());
+                    presenter.addConferenceParticipant(path.getAccountId(), path.getConversationUri());
                 }
             }
         } else if (requestCode == REQUEST_CODE_SCREEN_SHARE) {
@@ -1315,9 +1315,11 @@ public class CallFragment extends BaseSupportFragment<CallPresenter> implements
             Bundle args;
             args = getArguments();
             if (args != null) {
-                presenter.initOutGoing(getArguments().getString(KEY_ACCOUNT_ID),
-                        getArguments().getString(ConversationFragment.KEY_CONTACT_RING_ID),
-                        getArguments().getBoolean(KEY_AUDIO_ONLY));
+                ConversationPath conversation = ConversationPath.fromBundle(args);
+                presenter.initOutGoing(conversation.getAccountId(),
+                        conversation.getConversationUri(),
+                        args.getString(Intent.EXTRA_PHONE_NUMBER),
+                        args.getBoolean(KEY_AUDIO_ONLY));
             }
         }
     }
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
new file mode 100644
index 0000000000000000000000000000000000000000..97195ac65a7727cbe3e5bfb42c09585090dfc5e8
--- /dev/null
+++ b/ring-android/app/src/main/java/cx/ring/fragments/ContactPickerFragment.java
@@ -0,0 +1,174 @@
+package cx.ring.fragments;
+
+import android.app.Dialog;
+import android.os.Bundle;
+
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.google.android.material.bottomsheet.BottomSheetBehavior;
+import com.google.android.material.bottomsheet.BottomSheetDialog;
+import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
+import com.google.android.material.chip.Chip;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import javax.inject.Inject;
+
+import cx.ring.R;
+import cx.ring.adapters.SmartListAdapter;
+import cx.ring.application.JamiApplication;
+import cx.ring.client.HomeActivity;
+import cx.ring.contacts.AvatarFactory;
+import cx.ring.databinding.FragContactPickerBinding;
+import cx.ring.facades.ConversationFacade;
+import cx.ring.model.CallContact;
+import cx.ring.smartlist.SmartListViewModel;
+import cx.ring.viewholders.SmartListViewHolder;
+import cx.ring.views.AvatarDrawable;
+import io.reactivex.Observable;
+import io.reactivex.Single;
+import io.reactivex.android.schedulers.AndroidSchedulers;
+import io.reactivex.disposables.CompositeDisposable;
+
+public class ContactPickerFragment extends BottomSheetDialogFragment {
+
+    public static final String TAG = ContactPickerFragment.class.getSimpleName();
+
+    private FragContactPickerBinding binding = null;
+    private SmartListAdapter adapter;
+    private final CompositeDisposable mDisposableBag = new CompositeDisposable();
+
+    private String mAccountId =  null;
+    private final Set<CallContact> mCurrentSelection = new HashSet<>();
+
+    @Inject
+    ConversationFacade mConversationFacade;
+
+    public ContactPickerFragment() {
+        // Required empty public constructor
+    }
+
+    public static ContactPickerFragment newInstance() {
+        return new ContactPickerFragment();
+    }
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setRetainInstance(true);
+        ((JamiApplication) getActivity().getApplication()).getInjectionComponent().inject(this);
+    }
+
+    @Override
+    public void onSaveInstanceState(@NonNull Bundle outState) {
+        super.onSaveInstanceState(outState);
+
+    }
+
+    @NonNull
+    @Override
+    public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
+        Dialog bdialog = super.onCreateDialog(savedInstanceState);
+        if (bdialog instanceof BottomSheetDialog) {
+            BottomSheetDialog dialog = (BottomSheetDialog) bdialog;
+            BottomSheetBehavior<FrameLayout> behavior = dialog.getBehavior();
+            behavior.setFitToContents(false);
+            behavior.setSkipCollapsed(true);
+            behavior.setState(BottomSheetBehavior.STATE_HALF_EXPANDED);
+        }
+        return bdialog;
+    }
+
+    @Override
+    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
+        mDisposableBag.add(mConversationFacade.getContactList()
+                .observeOn(AndroidSchedulers.mainThread())
+                .subscribe(conversations -> {
+                    if (binding == null)
+                        return;
+                    adapter.update(conversations);
+                }));
+    }
+
+    @Override
+    public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+        binding = FragContactPickerBinding.inflate(getLayoutInflater(), container, false);
+        adapter = new SmartListAdapter(null, new SmartListViewHolder.SmartListListeners() {
+            @Override
+            public void onItemClick(SmartListViewModel item) {
+                mAccountId = item.getAccountId();
+
+                boolean checked = !item.isChecked();
+                item.setChecked(checked);
+                adapter.update(item);
+
+                Runnable remover = () -> {
+                    mCurrentSelection.remove(item.getContact().get(0));
+                    if (mCurrentSelection.isEmpty())
+                        binding.createGroupBtn.setEnabled(false);
+                    item.setChecked(false);
+                    adapter.update(item);
+                    View v = binding.selectedContacts.findViewWithTag(item);
+                    if (v != null)
+                        binding.selectedContacts.removeView(v);
+                };
+
+                if (checked) {
+                    if (mCurrentSelection.add(item.getContact().get(0))) {
+                        Chip chip = new Chip(requireContext(), null, R.style.Widget_MaterialComponents_Chip_Entry);
+                        chip.setText(item.getContactName());
+                        chip.setChipIcon(new AvatarDrawable.Builder()
+                                .withViewModel(item)
+                                .withCircleCrop(true)
+                                .withCheck(false)
+                                .build(binding.getRoot().getContext()));
+                        chip.setCloseIconVisible(true);
+                        chip.setTag(item);
+                        chip.setOnCloseIconClickListener(v -> remover.run());
+                        binding.selectedContacts.addView(chip);
+                    }
+                    binding.createGroupBtn.setEnabled(true);
+                } else {
+                    remover.run();
+                }
+            }
+
+            @Override
+            public void onItemLongClick(SmartListViewModel item) {
+
+            }
+        });
+        binding.createGroupBtn.setOnClickListener(v -> mDisposableBag.add(mConversationFacade.createConversation(mAccountId, mCurrentSelection)
+                .observeOn(AndroidSchedulers.mainThread())
+                .subscribe(conversation -> {
+                    ((HomeActivity) requireActivity()).startConversation(mAccountId, conversation.getUri());
+                    Dialog dialog = getDialog();
+                    if (dialog != null)
+                        dialog.cancel();
+                })));
+        binding.contactList.setAdapter(adapter);
+        return binding.getRoot();
+    }
+
+    @Override
+    public void onDestroyView() {
+        mDisposableBag.clear();
+        binding = null;
+        adapter = null;
+        super.onDestroyView();
+    }
+
+    @Override
+    public int getTheme() {
+        return R.style.BottomSheetDialogTheme;
+    }
+}
\ No newline at end of file
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 967e90ea47e708e419679d2e8c68e45914c55d76..3280919e8378f025a7f9e14f9235b769df726216 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
@@ -66,10 +66,13 @@ import androidx.appcompat.view.menu.MenuBuilder;
 import androidx.appcompat.view.menu.MenuPopupHelper;
 import androidx.appcompat.widget.PopupMenu;
 import androidx.appcompat.widget.Toolbar;
+import androidx.core.util.Pair;
 import androidx.core.view.ViewCompat;
 import androidx.fragment.app.Fragment;
 import androidx.fragment.app.FragmentManager;
 import androidx.recyclerview.widget.DefaultItemAnimator;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
 
 import java.io.File;
 import java.io.IOException;
@@ -80,7 +83,6 @@ import java.util.Map;
 import cx.ring.BuildConfig;
 import cx.ring.R;
 import cx.ring.adapters.ConversationAdapter;
-import cx.ring.adapters.NumberAdapter;
 import cx.ring.application.JamiApplication;
 import cx.ring.client.CallActivity;
 import cx.ring.client.ContactDetailsActivity;
@@ -101,6 +103,7 @@ import cx.ring.model.Phone;
 import cx.ring.model.Error;
 import cx.ring.model.Uri;
 import cx.ring.mvp.BaseSupportFragment;
+import cx.ring.service.DRingService;
 import cx.ring.services.LocationSharingService;
 import cx.ring.services.NotificationService;
 import cx.ring.services.NotificationServiceImpl;
@@ -112,9 +115,11 @@ import cx.ring.utils.ConversationPath;
 import cx.ring.utils.MediaButtonsHelper;
 import cx.ring.views.AvatarDrawable;
 import io.reactivex.Completable;
+import io.reactivex.Observable;
 import io.reactivex.Single;
 import io.reactivex.android.schedulers.AndroidSchedulers;
 import io.reactivex.disposables.CompositeDisposable;
+import io.reactivex.schedulers.Schedulers;
 
 import static android.app.Activity.RESULT_OK;
 
@@ -129,6 +134,7 @@ public class ConversationFragment extends BaseSupportFragment<ConversationPresen
     public static final String KEY_ACCOUNT_ID = BuildConfig.APPLICATION_ID + ".ACCOUNT_ID";
     public static final String KEY_PREFERENCE_PENDING_MESSAGE = "pendingMessage";
     public static final String KEY_PREFERENCE_CONVERSATION_COLOR = "color";
+    public static final String KEY_PREFERENCE_CONVERSATION_LAST_READ = "lastRead";
     public static final String EXTRA_SHOW_MAP = "showMap";
 
     private static final int REQUEST_CODE_FILE_PICKER = 1000;
@@ -163,6 +169,9 @@ public class ConversationFragment extends BaseSupportFragment<ConversationPresen
     private final Map<String, AvatarDrawable> mParticipantAvatars = new HashMap<>();
     private final Map<String, AvatarDrawable> mSmallParticipantAvatars = new HashMap<>();
     private int mapWidth, mapHeight;
+    private String mLastRead;
+
+    private boolean loading = true;
 
     public AvatarDrawable getConversationAvatar(String uri) {
         return mParticipantAvatars.get(uri);
@@ -190,6 +199,7 @@ public class ConversationFragment extends BaseSupportFragment<ConversationPresen
             binding.pbLoading.setVisibility(View.GONE);
         if (mAdapter != null) {
             mAdapter.updateDataset(conversation);
+            loading = false;
         }
         requireActivity().invalidateOptionsMenu();
     }
@@ -329,6 +339,25 @@ public class ConversationFragment extends BaseSupportFragment<ConversationPresen
             }
         });
 
+        binding.histList.addOnScrollListener(new RecyclerView.OnScrollListener() {
+            // The minimum amount of items to have below current scroll position
+            // before loading more.
+            static private final int visibleThreshold = 3;
+
+            @Override
+            public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
+            }
+
+            @Override
+            public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
+                LinearLayoutManager layoutManager = (LinearLayoutManager) recyclerView.getLayoutManager();
+                if (!loading && layoutManager.findFirstVisibleItemPosition() < visibleThreshold) {
+                    loading = true;
+                    presenter.loadMore();
+                }
+            }
+        });
+
         DefaultItemAnimator animator = (DefaultItemAnimator) binding.histList.getItemAnimator();
         if (animator != null)
             animator.setSupportsChangeAnimations(false);
@@ -697,8 +726,11 @@ public class ConversationFragment extends BaseSupportFragment<ConversationPresen
 
     @Override
     public void addElement(Interaction element) {
-        mAdapter.add(element);
-        scrollToEnd();
+        if (mLastRead != null && mLastRead.equals(element.getMessageId()))
+            element.read();
+        if (mAdapter.add(element))
+            scrollToEnd();
+        loading = false;
     }
 
     @Override
@@ -723,6 +755,24 @@ public class ConversationFragment extends BaseSupportFragment<ConversationPresen
         mAdapter.setLastDisplayed(interaction);
     }
 
+    @Override
+    public void acceptFile(String accountId, Uri conversationUri, DataTransfer transfer) {
+        File cacheDir = requireContext().getCacheDir();
+        long spaceLeft = AndroidFileUtils.getSpaceLeft(cacheDir.toString());
+        if (spaceLeft == -1L || transfer.getTotalSize() > spaceLeft) {
+            presenter.noSpaceLeft();
+            return;
+        }
+        requireActivity().startService(new Intent(DRingService.ACTION_FILE_ACCEPT, ConversationPath.toUri(accountId, conversationUri), requireContext(), DRingService.class)
+                .putExtra(DRingService.KEY_TRANSFER_ID, transfer.getDaemonId()));
+    }
+
+    @Override
+    public void refuseFile(String accountId, Uri conversationUri, DataTransfer transfer) {
+        requireActivity().startService(new Intent(DRingService.ACTION_FILE_CANCEL, ConversationPath.toUri(accountId, conversationUri), requireContext(), DRingService.class)
+                .putExtra(DRingService.KEY_TRANSFER_ID, transfer.getDaemonId()));
+    }
+
     @Override
     public void shareFile(File path) {
         Context c = getContext();
@@ -841,13 +891,15 @@ public class ConversationFragment extends BaseSupportFragment<ConversationPresen
         if (path == null)
             return;
 
-        Uri contactUri = path.getConversationUri();
+        Uri uri = path.getConversationUri();
         mAdapter = new ConversationAdapter(this, presenter);
-        presenter.init(contactUri, path.getAccountId());
+        presenter.init(uri, path.getAccountId());
         try {
-            mPreferences = requireActivity().getSharedPreferences(path.getAccountId() + "_" + contactUri.getRawRingId(), Context.MODE_PRIVATE);
+            mPreferences = requireActivity().getSharedPreferences(path.getAccountId() + "_" + uri.getUri(), Context.MODE_PRIVATE);
             mPreferences.registerOnSharedPreferenceChangeListener(this);
             presenter.setConversationColor(mPreferences.getInt(KEY_PREFERENCE_CONVERSATION_COLOR, getResources().getColor(R.color.color_primary_light)));
+            mLastRead = mPreferences.getString(KEY_PREFERENCE_CONVERSATION_LAST_READ, null);
+            Log.w(TAG, "Loaded last read " + mLastRead);
         } catch (Exception e) {
             Log.e(TAG, "Can't load conversation preferences");
         }
@@ -861,7 +913,7 @@ public class ConversationFragment extends BaseSupportFragment<ConversationPresen
                     LocationSharingService locationService = binder.getService();
                     ConversationPath path = new ConversationPath(presenter.getPath());
                     if (locationService.isSharing(path)) {
-                        showMap(path.getAccountId(), contactUri.getUri(), false);
+                        showMap(path.getAccountId(), uri.getUri(), false);
                     }
                     try {
                         requireContext().unbindService(locationServiceConnection);
@@ -892,35 +944,33 @@ public class ConversationFragment extends BaseSupportFragment<ConversationPresen
     }
 
     @Override
-    public void displayContact(final CallContact contact) {
-        mCompositeDisposable.clear();
-        mCompositeDisposable.add(AvatarFactory.getAvatar(requireContext(), contact)
-                .doOnSuccess(d -> {
-                    mConversationAvatar = (AvatarDrawable) d;
-                    mParticipantAvatars.put(contact.getPrimaryNumber(),
-                            new AvatarDrawable((AvatarDrawable) d));
-                })
-                .flatMapObservable(d -> contact.getUpdatesSubject())
+    public void updateContact(CallContact contact) {
+        String contactKey = contact.getPrimaryNumber();
+        AvatarDrawable a = mSmallParticipantAvatars.get(contactKey);
+        if (a != null) {
+            a.update(contact);
+            mParticipantAvatars.get(contactKey).update(contact);
+            mAdapter.setPhoto();
+        } else {
+            mCompositeDisposable.add(AvatarFactory.getAvatar(requireContext(), contact, true)
+                    .subscribeOn(Schedulers.computation())
+                    .observeOn(AndroidSchedulers.mainThread())
+                    .subscribe(avatar -> {
+                        mParticipantAvatars.put(contactKey, (AvatarDrawable) avatar);
+                        mSmallParticipantAvatars.put(contactKey, new AvatarDrawable((AvatarDrawable) avatar));
+                        mAdapter.setPhoto();
+                    }));
+        }
+    }
+
+    @Override
+    public void displayContact(Conversation conversation) {
+        mCompositeDisposable.add(AvatarFactory.getAvatar(requireContext(), conversation, true)
                 .observeOn(AndroidSchedulers.mainThread())
-                .subscribe(c -> {
-                    mConversationAvatar.update(c);
-                    String uri = contact.getPrimaryNumber();
-                    AvatarDrawable ad = mParticipantAvatars.get(uri);
-                    if (ad != null)
-                        ad.update(c);
-                    setupActionbar(contact);
-                    mAdapter.setPhoto();
-                }));
-        mCompositeDisposable.add(AvatarFactory.getAvatar(requireContext(), contact, false)
-                .doOnSuccess(d -> mSmallParticipantAvatars.put(contact.getPrimaryNumber(), new AvatarDrawable((AvatarDrawable) d)))
-                .flatMapObservable(d -> contact.getUpdatesSubject())
-                .subscribe(c -> {
-                    synchronized (mSmallParticipantAvatars) {
-                        String uri = contact.getPrimaryNumber();
-                        AvatarDrawable ad = mSmallParticipantAvatars.get(uri);
-                        if (ad != null)
-                            ad.update(c);
-                    }
+                .subscribe(d -> {
+                    mConversationAvatar = (AvatarDrawable) d;
+                    mParticipantAvatars.put(conversation.getUri().getRawRingId(), new AvatarDrawable((AvatarDrawable) d));
+                    setupActionbar(conversation);
                 }));
     }
 
@@ -932,8 +982,7 @@ public class ConversationFragment extends BaseSupportFragment<ConversationPresen
     @Override
     public void displayNumberSpinner(final Conversation conversation, final Uri number) {
         binding.numberSelector.setVisibility(View.VISIBLE);
-        binding.numberSelector.setAdapter(new NumberAdapter(getActivity(),
-                conversation.getContact(), false));
+        //binding.numberSelector.setAdapter(new NumberAdapter(getActivity(), conversation.getContact(), false));
         binding.numberSelector.setSelection(getIndex(binding.numberSelector, number));
     }
 
@@ -967,22 +1016,22 @@ public class ConversationFragment extends BaseSupportFragment<ConversationPresen
     }
 
     @Override
-    public void goToContactActivity(String accountId, String contactId) {
-        startActivity(new Intent(Intent.ACTION_VIEW, ConversationPath.toUri(accountId, contactId),
-                requireActivity().getApplicationContext(), ContactDetailsActivity.class));
+    public void goToContactActivity(String accountId, Uri uri) {
+        startActivity(new Intent(Intent.ACTION_VIEW, ConversationPath.toUri(accountId, uri))
+                .setClass(requireActivity().getApplicationContext(), ContactDetailsActivity.class));
     }
 
     @Override
-    public void goToCallActivityWithResult(String accountId, String contactRingId, boolean audioOnly) {
-        Intent intent = new Intent(CallActivity.ACTION_CALL)
-                .setClass(requireActivity().getApplicationContext(), CallActivity.class)
-                .putExtra(KEY_ACCOUNT_ID, accountId)
-                .putExtra(CallFragment.KEY_AUDIO_ONLY, audioOnly)
-                .putExtra(KEY_CONTACT_RING_ID, contactRingId);
+    public void goToCallActivityWithResult(String accountId, Uri conversationUri, Uri contactUri, boolean audioOnly) {
+        Intent intent = new Intent(Intent.ACTION_CALL)
+                .setClass(requireContext(), CallActivity.class)
+                .putExtras(ConversationPath.toBundle(accountId, conversationUri))
+                .putExtra(Intent.EXTRA_PHONE_NUMBER, contactUri.getUri())
+                .putExtra(CallFragment.KEY_AUDIO_ONLY, audioOnly);
         startActivityForResult(intent, HomeActivity.REQUEST_CODE_CALL);
     }
 
-    private void setupActionbar(CallContact contact) {
+    private void setupActionbar(Conversation conversation) {
         if (!isVisible()) {
             return;
         }
@@ -993,8 +1042,9 @@ public class ConversationFragment extends BaseSupportFragment<ConversationPresen
         }
 
         Context context = actionBar.getThemedContext();
-        String displayName = contact.getDisplayName();
-        String identity = contact.getRingUsername();
+
+        String displayName = conversation.getTitle();
+        String identity = conversation.getUriTitle();
 
         Activity activity = getActivity();
         if (activity instanceof HomeActivity) {
@@ -1003,12 +1053,12 @@ public class ConversationFragment extends BaseSupportFragment<ConversationPresen
             TextView subtitle = toolbar.findViewById(R.id.contact_subtitle);
             ImageView logo = toolbar.findViewById(R.id.contact_image);
 
-            if (!((HomeActivity) activity).isConversationSelected()) {
+            /*if (!((HomeActivity) activity).isConversationSelected()) {
                 title.setText("");
                 subtitle.setText("");
                 logo.setImageDrawable(null);
                 return;
-            }
+            }*/
 
             logo.setVisibility(View.VISIBLE);
             title.setText(displayName);
@@ -1217,6 +1267,14 @@ public class ConversationFragment extends BaseSupportFragment<ConversationPresen
         }
     }
 
+    @Override
+    public void updateLastRead(String last) {
+        Log.w(TAG, "Updated last read " + mLastRead);
+        mLastRead = last;
+        if (mPreferences != null)
+            mPreferences.edit().putString(KEY_PREFERENCE_CONVERSATION_LAST_READ, last).apply();
+    }
+
     @Override
     public void hideErrorPanel() {
         if (binding != null) {
diff --git a/ring-android/app/src/main/java/cx/ring/fragments/LocationSharingFragment.java b/ring-android/app/src/main/java/cx/ring/fragments/LocationSharingFragment.java
index 496d745f16a1912f3af7f11222570111a13769ce..0dbd3162c6cc57ef545c64ea9b143f310956ac7a 100644
--- a/ring-android/app/src/main/java/cx/ring/fragments/LocationSharingFragment.java
+++ b/ring-android/app/src/main/java/cx/ring/fragments/LocationSharingFragment.java
@@ -40,7 +40,6 @@ import androidx.annotation.RequiresApi;
 import androidx.core.app.ActivityCompat;
 import androidx.core.content.ContextCompat;
 import androidx.fragment.app.Fragment;
-import androidx.preference.PreferenceManager;
 
 import android.os.IBinder;
 import android.text.format.DateUtils;
@@ -326,7 +325,7 @@ public class LocationSharingFragment extends Fragment {
                     }
                 }));
 
-        final Uri contactUri = new Uri(mPath.getContactId());
+        final Uri contactUri = mPath.getConversationUri();
 
         mDisposableBag.add(mConversationFacade
                 .getAccountSubject(mPath.getAccountId())
diff --git a/ring-android/app/src/main/java/cx/ring/fragments/QRCodeFragment.java b/ring-android/app/src/main/java/cx/ring/fragments/QRCodeFragment.java
index 4db6ab58144fb487f15c6955c5a01972eafdfe29..cc60aa74c1d82f9d8fb2799e258fff77914d87d7 100644
--- a/ring-android/app/src/main/java/cx/ring/fragments/QRCodeFragment.java
+++ b/ring-android/app/src/main/java/cx/ring/fragments/QRCodeFragment.java
@@ -53,11 +53,9 @@ public class QRCodeFragment extends BottomSheetDialogFragment {
 
     public static QRCodeFragment newInstance(int startPage) {
         QRCodeFragment fragment = new QRCodeFragment();
-
         Bundle args = new Bundle();
         args.putInt(ARG_START_PAGE_INDEX, startPage);
         fragment.setArguments(args);
-
         return fragment;
     }
 
@@ -75,7 +73,6 @@ public class QRCodeFragment extends BottomSheetDialogFragment {
         mBinding = FragQrcodeBinding.inflate(inflater, container, false);
         mBinding.viewPager.setAdapter(new SectionsPagerAdapter(getContext(), getChildFragmentManager()));
         mBinding.tabs.setupWithViewPager(mBinding.viewPager);
-
         return mBinding.getRoot();
     }
 
diff --git a/ring-android/app/src/main/java/cx/ring/fragments/ShareWithFragment.java b/ring-android/app/src/main/java/cx/ring/fragments/ShareWithFragment.java
index 5403e187f19acacde5ca737592faea1010285ece..7acb77a7f0a0b86479305d414f0ffd0c793b5e05 100644
--- a/ring-android/app/src/main/java/cx/ring/fragments/ShareWithFragment.java
+++ b/ring-android/app/src/main/java/cx/ring/fragments/ShareWithFragment.java
@@ -146,7 +146,7 @@ public class ShareWithFragment extends Fragment {
                         intent.putExtra(Intent.EXTRA_TEXT, previewText.getText().toString());
                     }
                     intent.putExtra(ConversationFragment.KEY_ACCOUNT_ID, smartListViewModel.getAccountId());
-                    intent.putExtra(ConversationFragment.KEY_CONTACT_RING_ID, smartListViewModel.getContact().getPrimaryNumber());
+                    intent.putExtra(ConversationFragment.KEY_CONTACT_RING_ID, smartListViewModel.getUri().getUri());
                     intent.setClass(requireActivity(), ConversationActivity.class);
                     startActivity(intent);
                 }
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 c01e93690f762386b9268e4f3e09672aac02d178..d8671a9a03d1bc6a81d51208edcc4fc8b6720198 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
@@ -63,10 +63,8 @@ import cx.ring.R;
 import cx.ring.adapters.SmartListAdapter;
 import cx.ring.application.JamiApplication;
 import cx.ring.client.CallActivity;
-import cx.ring.client.ConversationActivity;
 import cx.ring.client.HomeActivity;
 import cx.ring.databinding.FragSmartlistBinding;
-import cx.ring.model.CallContact;
 import cx.ring.model.Conversation;
 import cx.ring.mvp.BaseSupportFragment;
 import cx.ring.services.AccountService;
@@ -116,6 +114,7 @@ public class SmartListFragment extends BaseSupportFragment<SmartListPresenter> i
                 setOverflowMenuVisible(menu, true);
                 changeSeparatorHeight(false);
                 binding.qrCode.setVisibility(View.GONE);
+                binding.newGroup.setVisibility(View.GONE);
                 setTabletQRLayout(false);
                 return true;
             }
@@ -127,6 +126,7 @@ public class SmartListFragment extends BaseSupportFragment<SmartListPresenter> i
                 setOverflowMenuVisible(menu, false);
                 changeSeparatorHeight(true);
                 binding.qrCode.setVisibility(View.VISIBLE);
+                binding.newGroup.setVisibility(View.VISIBLE);
                 setTabletQRLayout(true);
                 return true;
             }
@@ -245,6 +245,8 @@ public class SmartListFragment extends BaseSupportFragment<SmartListPresenter> i
 
         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) {
@@ -276,6 +278,14 @@ public class SmartListFragment extends BaseSupportFragment<SmartListPresenter> i
         binding.newconvFab.setOnClickListener(v -> presenter.fabButtonClicked());
     }
 
+    private void startNewGroup() {
+        ContactPickerFragment fragment = ContactPickerFragment.newInstance();
+        fragment.show(getParentFragmentManager(), ContactPickerFragment.TAG);
+        binding.qrCode.setVisibility(View.GONE);
+        binding.newGroup.setVisibility(View.GONE);
+        setTabletQRLayout(false);
+    }
+
     @Override
     public void setLoading(final boolean loading) {
         binding.loadingIndicator.setVisibility(loading ? View.VISIBLE : View.GONE);
@@ -297,12 +307,12 @@ public class SmartListFragment extends BaseSupportFragment<SmartListPresenter> i
     }
 
     @Override
-    public void removeConversation(CallContact callContact) {
+    public void removeConversation(cx.ring.model.Uri callContact) {
         presenter.removeConversation(callContact);
     }
 
     @Override
-    public void clearConversation(CallContact callContact) {
+    public void clearConversation(cx.ring.model.Uri callContact) {
         presenter.clearConversation(callContact);
     }
 
@@ -370,18 +380,18 @@ public class SmartListFragment extends BaseSupportFragment<SmartListPresenter> i
     }
 
     @Override
-    public void displayClearDialog(CallContact callContact) {
-        ActionHelper.launchClearAction(getActivity(), callContact, SmartListFragment.this);
+    public void displayClearDialog(cx.ring.model.Uri uri) {
+        ActionHelper.launchClearAction(getActivity(), uri, SmartListFragment.this);
     }
 
     @Override
-    public void displayDeleteDialog(CallContact callContact) {
-        ActionHelper.launchDeleteAction(getActivity(), callContact, SmartListFragment.this);
+    public void displayDeleteDialog(cx.ring.model.Uri uri) {
+        ActionHelper.launchDeleteAction(getActivity(), uri, SmartListFragment.this);
     }
 
     @Override
-    public void copyNumber(CallContact callContact) {
-        ActionHelper.launchCopyNumberToClipboardFromContact(getActivity(), callContact, this);
+    public void copyNumber(cx.ring.model.Uri uri) {
+        ActionHelper.launchCopyNumberToClipboardFromContact(getActivity(), uri, this);
     }
 
     @Override
@@ -431,33 +441,29 @@ public class SmartListFragment extends BaseSupportFragment<SmartListPresenter> i
     public void onActivityResult(int requestCode, int resultCode, Intent data) {
         super.onActivityResult(requestCode, resultCode, data);
         if (requestCode == HomeActivity.REQUEST_CODE_QR_CONVERSATION && data != null && resultCode == Activity.RESULT_OK) {
-            String contactID = data.getStringExtra(ConversationFragment.KEY_CONTACT_RING_ID);
-            if (contactID != null) {
-                presenter.startConversation(new cx.ring.model.Uri(contactID));
+            String contactId = data.getStringExtra(ConversationFragment.KEY_CONTACT_RING_ID);
+            if (contactId != null) {
+                presenter.startConversation(cx.ring.model.Uri.fromString(contactId));
             }
         }
     }
 
     @Override
-    public void goToConversation(String accountId, cx.ring.model.Uri contactId) {
+    public void goToConversation(String accountId, cx.ring.model.Uri conversationUri) {
+        Log.w(TAG, "goToConversation " + accountId + " " + conversationUri);
         if (mSearchMenuItem != null) {
             mSearchMenuItem.collapseActionView();
         }
-
-        if (!DeviceUtils.isTablet(getContext())) {
-            startActivity(new Intent(Intent.ACTION_VIEW, ConversationPath.toUri(accountId, contactId.toString()), requireContext(), ConversationActivity.class));
-        } else {
-            ((HomeActivity) requireActivity()).startConversationTablet(ConversationPath.toBundle(accountId, contactId.toString()));
-        }
+        ((HomeActivity) requireActivity()).startConversation(accountId, conversationUri);
     }
 
     @Override
-    public void goToCallActivity(String accountId, String contactId) {
+    public void goToCallActivity(String accountId, cx.ring.model.Uri conversationUri, String contactId) {
         Intent intent = new Intent(CallActivity.ACTION_CALL)
-                .setClass(requireActivity(), CallActivity.class)
+                .setClass(requireContext(), CallActivity.class)
+                .putExtras(ConversationPath.toBundle(accountId, conversationUri))
                 .putExtra(CallFragment.KEY_AUDIO_ONLY, false)
-                .putExtra(ConversationFragment.KEY_ACCOUNT_ID, accountId)
-                .putExtra(ConversationFragment.KEY_CONTACT_RING_ID, contactId);
+                .putExtra(Intent.EXTRA_PHONE_NUMBER, contactId);
         startActivityForResult(intent, HomeActivity.REQUEST_CODE_CALL);
     }
 
@@ -466,6 +472,7 @@ public class SmartListFragment extends BaseSupportFragment<SmartListPresenter> i
         QRCodeFragment qrCodeFragment = QRCodeFragment.newInstance(QRCodeFragment.INDEX_SCAN);
         qrCodeFragment.show(getParentFragmentManager(), QRCodeFragment.TAG);
         binding.qrCode.setVisibility(View.GONE);
+        binding.newGroup.setVisibility(View.GONE);
         setTabletQRLayout(false);
     }
 
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 482b24065500519cd557b0a9baf4e4b6c6e269df..e2e01279f14ffa0241fbb96c99a84cbc838bfc99 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
@@ -143,7 +143,7 @@ public class DRingService extends Service {
 
         @Override
         public String placeCall(final String account, final String number, final boolean video) {
-            return mConversationFacade.placeCall(account, number, video).blockingGet().getDaemonIdString();
+            return mConversationFacade.placeCall(account, Uri.fromString(number), video).blockingGet().getDaemonIdString();
         }
 
         @Override
@@ -621,9 +621,7 @@ public class DRingService extends Service {
             case ACTION_TRUST_REQUEST_ACCEPT:
             case ACTION_TRUST_REQUEST_REFUSE:
             case ACTION_TRUST_REQUEST_BLOCK:
-                if (extras != null) {
-                    handleTrustRequestAction(action, extras);
-                }
+                handleTrustRequestAction(intent.getData(), action);
                 break;
             case ACTION_CALL_ACCEPT:
             case ACTION_CALL_HOLD_ACCEPT:
@@ -639,12 +637,12 @@ public class DRingService extends Service {
             case ACTION_CONV_ACCEPT:
             case ACTION_CONV_DISMISS:
             case ACTION_CONV_REPLY_INLINE:
-                handleConvAction(intent, action, extras);
+                handleConvAction(intent, action);
                 break;
             case ACTION_FILE_ACCEPT:
             case ACTION_FILE_CANCEL:
                 if (extras != null) {
-                    handleFileAction(action, extras);
+                    handleFileAction(intent.getData(), action, extras);
                 }
                 break;
             default:
@@ -652,30 +650,30 @@ public class DRingService extends Service {
         }
     }
 
-    private void handleFileAction(String action, Bundle extras) {
-        Long id = extras.getLong(KEY_TRANSFER_ID);
+    private void handleFileAction(android.net.Uri uri, String action, Bundle extras) {
+        long id = extras.getLong(KEY_TRANSFER_ID);
+        ConversationPath path = ConversationPath.fromUri(uri);
         if (action.equals(ACTION_FILE_ACCEPT)) {
-            mAccountService.acceptFileTransfer(id);
+            mAccountService.acceptFileTransfer(path.getAccountId(), path.getConversationUri(), id);
         } else if (action.equals(ACTION_FILE_CANCEL)) {
-            mConversationFacade.cancelFileTransfer(id);
+            mConversationFacade.cancelFileTransfer(path.getAccountId(), path.getConversationUri(), id);
         }
     }
 
-    private void handleTrustRequestAction(String action, Bundle extras) {
-        String account = extras.getString(NotificationService.TRUST_REQUEST_NOTIFICATION_ACCOUNT_ID);
-        Uri from = new Uri(extras.getString(NotificationService.TRUST_REQUEST_NOTIFICATION_FROM));
-        if (account != null) {
-            mNotificationService.cancelTrustRequestNotification(account);
+    private void handleTrustRequestAction(android.net.Uri uri, String action) {
+        ConversationPath path = ConversationPath.fromUri(uri);
+        if (path != null) {
+            mNotificationService.cancelTrustRequestNotification(path.getAccountId());
             switch (action) {
                 case ACTION_TRUST_REQUEST_ACCEPT:
-                    mConversationFacade.acceptRequest(account, from);
+                    mConversationFacade.acceptRequest(path.getAccountId(), path.getConversationUri());
                     break;
                 case ACTION_TRUST_REQUEST_REFUSE:
-                    mConversationFacade.discardRequest(account, from);
+                    mConversationFacade.discardRequest(path.getAccountId(), path.getConversationUri());
                     break;
                 case ACTION_TRUST_REQUEST_BLOCK:
-                    mConversationFacade.discardRequest(account, from);
-                    mAccountService.removeContact(account, from.getRawRingId(), true);
+                    mConversationFacade.discardRequest(path.getAccountId(), path.getConversationUri());
+                    mAccountService.removeContact(path.getAccountId(), path.getConversationUri().getRawRingId(), true);
                     break;
             }
         }
@@ -740,16 +738,15 @@ public class DRingService extends Service {
         }
     }
 
-    private void handleConvAction(Intent intent, String action, Bundle extras) {
+    private void handleConvAction(Intent intent, String action) {
         ConversationPath path = ConversationPath.fromIntent(intent);
-
         if (path == null || path.getConversationId().isEmpty()) {
             return;
         }
 
         switch (action) {
             case ACTION_CONV_READ:
-                mConversationFacade.readMessages(path.getAccountId(), new Uri(path.getConversationId()));
+                mConversationFacade.readMessages(path.getAccountId(), path.getConversationUri());
                 break;
             case ACTION_CONV_DISMISS:
                 break;
@@ -758,11 +755,11 @@ public class DRingService extends Service {
                 if (remoteInput != null) {
                     CharSequence reply = remoteInput.getCharSequence(KEY_TEXT_REPLY);
                     if (!TextUtils.isEmpty(reply)) {
-                        Uri uri = new Uri(path.getConversationId());
+                        Uri uri = path.getConversationUri();
                         String message = reply.toString();
                         mConversationFacade.startConversation(path.getAccountId(), uri)
-                                .flatMap(c -> mConversationFacade.sendTextMessage(path.getAccountId(), c, uri, message)
-                                        .doOnSuccess(msg -> mNotificationService.showTextNotification(path.getAccountId(), c)))
+                                .flatMapCompletable(c -> mConversationFacade.sendTextMessage(c, uri, message)
+                                        .doOnComplete(() -> mNotificationService.showTextNotification(path.getAccountId(), c)))
                                 .subscribe();
                     }
                 }
diff --git a/ring-android/app/src/main/java/cx/ring/service/OutgoingCallHandler.java b/ring-android/app/src/main/java/cx/ring/service/OutgoingCallHandler.java
index 366ab362495064c12a4a6bee99c8021affc89ee5..894fa5215d505bb1a6f8c9da78307edcbc9ed391 100644
--- a/ring-android/app/src/main/java/cx/ring/service/OutgoingCallHandler.java
+++ b/ring-android/app/src/main/java/cx/ring/service/OutgoingCallHandler.java
@@ -49,16 +49,14 @@ public class OutgoingCallHandler extends BroadcastReceiver {
             boolean systemDialerSip = sharedPreferences.getBoolean(KEY_CACHE_HAVE_SIPACCOUNT, false);
             boolean systemDialerRing = sharedPreferences.getBoolean(KEY_CACHE_HAVE_RINGACCOUNT, false);
 
-            Uri uri = new Uri(phoneNumber);
-            boolean isRingId = uri.isRingId();
+            Uri uri = Uri.fromString(phoneNumber);
+            boolean isRingId = uri.isHexId();
             if ((!isRingId && systemDialerSip) || (isRingId && systemDialerRing) || uri.isSingleIp()) {
-                Intent i = new Intent(CallActivity.ACTION_CALL)
+                Intent i = new Intent(Intent.ACTION_CALL)
                         .setClass(context, CallActivity.class)
                         .setData(android.net.Uri.parse(phoneNumber))
                         .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
-
                 context.startActivity(i);
-
                 setResultData(null);
             }
         }
diff --git a/ring-android/app/src/main/java/cx/ring/services/ContactServiceImpl.java b/ring-android/app/src/main/java/cx/ring/services/ContactServiceImpl.java
index d17ca172c33904377282aae9aa5f62dc1de14a7a..6e727d90eac9a0000ca750267335a3e538d5948d 100644
--- a/ring-android/app/src/main/java/cx/ring/services/ContactServiceImpl.java
+++ b/ring-android/app/src/main/java/cx/ring/services/ContactServiceImpl.java
@@ -39,7 +39,6 @@ import cx.ring.contacts.AvatarFactory;
 import cx.ring.model.CallContact;
 import cx.ring.model.Uri;
 import cx.ring.utils.AndroidFileUtils;
-import cx.ring.utils.BitmapUtils;
 import cx.ring.utils.Tuple;
 import cx.ring.utils.VCardUtils;
 import ezvcard.VCard;
@@ -136,21 +135,22 @@ public class ContactServiceImpl extends ContactService {
 
             while (contactCursor.moveToNext()) {
                 long contactId = contactCursor.getLong(indexId);
-                CallContact contact = cache.get(contactId);
 
+                String contactNumber = contactCursor.getString(indexNumber);
+                int contactType = contactCursor.getInt(indexType);
+                String contactLabel = contactCursor.getString(indexLabel);
+                Uri uri = Uri.fromString(contactNumber);
+
+                CallContact contact = cache.get(contactId);
                 boolean isNewContact = false;
                 if (contact == null) {
-                    contact = new CallContact(contactId);
+                    contact = new CallContact(uri);
+                    contact.setSystemId(contactId);
                     isNewContact = true;
                     contact.setFromSystem(true);
                 }
 
-                String contactNumber = contactCursor.getString(indexNumber);
-                int contactType = contactCursor.getInt(indexType);
-                String contactLabel = contactCursor.getString(indexLabel);
-                Uri uri = new Uri(contactNumber);
-
-                if (uri.isSingleIp() || (uri.isRingId() && loadRingContacts) || loadSipContacts) {
+                if (uri.isSingleIp() || (uri.isHexId() && loadRingContacts) || loadSipContacts) {
                     switch (contactCursor.getString(indexMime)) {
                         case ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE:
                             contact.addPhoneNumber(uri, contactType, contactLabel);
@@ -159,7 +159,7 @@ public class ContactServiceImpl extends ContactService {
                             contact.addNumber(uri, contactType, contactLabel, cx.ring.model.Phone.NumberType.SIP);
                             break;
                         case ContactsContract.CommonDataKinds.Im.CONTENT_ITEM_TYPE:
-                            if (uri.isRingId()) {
+                            if (uri.isHexId()) {
                                 contact.addNumber(uri, contactType, contactLabel, cx.ring.model.Phone.NumberType.UNKNOWN);
                             }
                             break;
@@ -195,7 +195,7 @@ public class ContactServiceImpl extends ContactService {
                 if (contact == null)
                     Log.w(TAG, "Can't find contact with ID " + contactId);
                 else {
-                    contact.setContactInfos(contactCursor.getString(indexKey), contactCursor.getString(indexName), contactCursor.getLong(indexPhoto));
+                    contact.setSystemContactInfo(contactId, contactCursor.getString(indexKey), contactCursor.getString(indexName), contactCursor.getLong(indexPhoto));
                     systemContacts.put(contactId, contact);
                 }
             }
@@ -240,7 +240,8 @@ public class ContactServiceImpl extends ContactService {
 
                 Log.d(TAG, "Contact name: " + result.getString(indexName) + " id:" + contactId + " key:" + result.getString(indexKey));
 
-                contact = new CallContact(contactId, result.getString(indexKey), result.getString(indexName), result.getLong(indexPhoto));
+                contact = new CallContact(Uri.fromString(contentUri.toString()));
+                contact.setSystemContactInfo(contactId, result.getString(indexKey), result.getString(indexName), result.getLong(indexPhoto));
 
                 if (result.getInt(indexStared) != 0) {
                     contact.setStared();
@@ -262,7 +263,6 @@ public class ContactServiceImpl extends ContactService {
     }
 
     private void fillContactDetails(@NonNull CallContact callContact) {
-
         ContentResolver contentResolver = mContext.getContentResolver();
 
         try {
@@ -303,7 +303,7 @@ public class ContactServiceImpl extends ContactService {
                     String contactMime = cursorSip.getString(indexMime);
                     String contactNumber = cursorSip.getString(indexSip);
 
-                    if (!contactMime.contentEquals(ContactsContract.CommonDataKinds.Im.CONTENT_ITEM_TYPE) || new Uri(contactNumber).isRingId() || "ring".equalsIgnoreCase(cursorSip.getString(indexLabel))) {
+                    if (!contactMime.contentEquals(ContactsContract.CommonDataKinds.Im.CONTENT_ITEM_TYPE) || Uri.fromString(contactNumber).isHexId() || "ring".equalsIgnoreCase(cursorSip.getString(indexLabel))) {
                         callContact.addNumber(contactNumber, cursorSip.getInt(indexType), cursorSip.getString(indexLabel), cx.ring.model.Phone.NumberType.SIP);
                     }
                     Log.d(TAG, "SIP phone:" + contactNumber + " " + contactMime + " ");
@@ -327,7 +327,7 @@ public class ContactServiceImpl extends ContactService {
 
             if (result == null) {
                 Log.d(TAG, "findContactBySipNumberFromSystem: " + number + " can't find contact.");
-                return CallContact.buildSIP(new Uri(number));
+                return CallContact.buildSIP(Uri.fromString(number));
             }
 
             int indexId = result.getColumnIndex(ContactsContract.RawContacts.CONTACT_ID);
@@ -338,7 +338,8 @@ public class ContactServiceImpl extends ContactService {
 
             if (result.moveToFirst()) {
                 long contactId = result.getLong(indexId);
-                contact = new CallContact(contactId, result.getString(indexKey), result.getString(indexName), result.getLong(indexPhoto));
+                contact = new CallContact(Uri.fromString(number));
+                contact.setSystemContactInfo(contactId, result.getString(indexKey), result.getString(indexName), result.getLong(indexPhoto));
 
                 if (result.getInt(indexStared) != 0) {
                     contact.setStared();
@@ -374,7 +375,8 @@ public class ContactServiceImpl extends ContactService {
                 int indexKey = result.getColumnIndex(ContactsContract.Data.LOOKUP_KEY);
                 int indexName = result.getColumnIndex(ContactsContract.Contacts.DISPLAY_NAME);
                 int indexPhoto = result.getColumnIndex(ContactsContract.Contacts.PHOTO_ID);
-                callContact = new CallContact(result.getLong(indexId), result.getString(indexKey), result.getString(indexName), result.getLong(indexPhoto));
+                callContact = new CallContact(Uri.fromString(number));
+                callContact.setSystemContactInfo(result.getLong(indexId), result.getString(indexKey), result.getString(indexName), result.getLong(indexPhoto));
                 fillContactDetails(callContact);
                 Log.d(TAG, "findContactByNumberFromSystem: " + number + " found " + callContact.getDisplayName());
             }
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 82c47dc6bb346cccfda474a0bc65c03b55030cb6..7e26fb49f01d3ab5b32b6b7e8a4e03b07353960d 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
@@ -29,6 +29,7 @@ import android.net.NetworkInfo;
 import android.os.Build;
 import android.provider.ContactsContract;
 import android.util.Log;
+import android.widget.Toast;
 
 import androidx.core.content.ContextCompat;
 
@@ -44,6 +45,7 @@ import cx.ring.daemon.StringVect;
 import cx.ring.utils.AndroidFileUtils;
 import cx.ring.utils.NetworkUtils;
 import cx.ring.utils.StringUtils;
+import io.reactivex.android.schedulers.AndroidSchedulers;
 
 public class DeviceRuntimeServiceImpl extends DeviceRuntimeService {
 
@@ -71,6 +73,8 @@ public class DeviceRuntimeServiceImpl extends DeviceRuntimeService {
                 System.loadLibrary("ring");
             } catch (Exception e) {
                 Log.e(TAG, "Could not load Jami library", e);
+                android.os.Process.killProcess(android.os.Process.myPid());
+                System.exit(0);
             }
         });
     }
diff --git a/ring-android/app/src/main/java/cx/ring/services/HistoryServiceImpl.java b/ring-android/app/src/main/java/cx/ring/services/HistoryServiceImpl.java
index 8528d723b66ad7d921cb721302628b027ebd8967..08bfb2055a3e5a5522d2a00774551d2636542fd6 100644
--- a/ring-android/app/src/main/java/cx/ring/services/HistoryServiceImpl.java
+++ b/ring-android/app/src/main/java/cx/ring/services/HistoryServiceImpl.java
@@ -22,6 +22,7 @@
 package cx.ring.services;
 
 import android.content.Context;
+import android.content.SharedPreferences;
 import android.database.sqlite.SQLiteDatabase;
 import android.database.sqlite.SQLiteException;
 import android.util.Log;
@@ -37,13 +38,18 @@ import java.util.concurrent.ConcurrentHashMap;
 
 import javax.inject.Inject;
 
+import cx.ring.R;
 import cx.ring.history.DatabaseHelper;
+import cx.ring.model.Conversation;
 import cx.ring.model.ConversationHistory;
 import cx.ring.model.Interaction;
+import cx.ring.model.Uri;
 import io.reactivex.Observable;
 import io.reactivex.subjects.BehaviorSubject;
 import io.reactivex.subjects.Subject;
 
+import static cx.ring.fragments.ConversationFragment.KEY_PREFERENCE_CONVERSATION_LAST_READ;
+
 /**
  * Implements the necessary Android related methods for the {@link HistoryService}
  */
@@ -183,6 +189,18 @@ public class HistoryServiceImpl extends HistoryService {
             deleteFolder(accountDir);
     }
 
+    @Override
+    public void setMessageRead(String accountId, Uri conversationUri, String lastId) {
+        SharedPreferences preferences = mContext.getSharedPreferences(accountId + "_" + conversationUri.getUri(), Context.MODE_PRIVATE);
+        preferences.edit().putString(KEY_PREFERENCE_CONVERSATION_LAST_READ, lastId).apply();
+    }
+
+    @Override
+    public String getLastMessageRead(String accountId, Uri conversationUri) {
+        SharedPreferences preferences = mContext.getSharedPreferences(accountId + "_" + conversationUri.getUri(), Context.MODE_PRIVATE);
+        return preferences.getString(KEY_PREFERENCE_CONVERSATION_LAST_READ, null);
+    }
+
     /**
      * Deletes a file and all its children recursively
      *
diff --git a/ring-android/app/src/main/java/cx/ring/services/LocationSharingService.java b/ring-android/app/src/main/java/cx/ring/services/LocationSharingService.java
index 11b5dbc172dc929817275c4559c99bf18a7a3c67..c849964d503596985353d59f267db1af5c4eef11 100644
--- a/ring-android/app/src/main/java/cx/ring/services/LocationSharingService.java
+++ b/ring-android/app/src/main/java/cx/ring/services/LocationSharingService.java
@@ -212,7 +212,7 @@ public class LocationSharingService extends Service implements LocationListener
                             StringMap msgs = new StringMap();
                             msgs.setRaw(CallService.MIME_GEOLOCATION, Blob.fromString(location.toString()));
                             for (ConversationPath p : contactLocationShare.keySet())  {
-                                Ringservice.sendAccountTextMessage(p.getAccountId(), p.getContactId(), msgs);
+                                Ringservice.sendAccountTextMessage(p.getAccountId(), p.getConversationId(), msgs);
                             }
                         }));
             } else {
@@ -237,7 +237,7 @@ public class LocationSharingService extends Service implements LocationListener
                 Log.w(TAG, "location send " + jsonObject + " to " + contactLocationShare.size());
                 StringMap msgs = new StringMap();
                 msgs.setRaw(CallService.MIME_GEOLOCATION, Blob.fromString(jsonObject.toString()));
-                Ringservice.sendAccountTextMessage(path.getAccountId(), path.getContactId(), msgs);
+                Ringservice.sendAccountTextMessage(path.getAccountId(), path.getConversationId(), msgs);
             }
 
             mContactSharingSubject.onNext(contactLocationShare.keySet());
@@ -310,7 +310,7 @@ public class LocationSharingService extends Service implements LocationListener
         // Log.w(TAG, "getNotification " + firsPath.getContactId());
 
         return mConversationFacade.getAccountSubject(firsPath.getAccountId())
-                .map(account -> account.getContactFromCache(new Uri(firsPath.getContactId())))
+                .map(account -> account.getContactFromCache(firsPath.getConversationUri()))
                 .map(contact -> {
                     String title;
                     final Intent stopIntent = new Intent(ACTION_STOP).setClass(getApplicationContext(), LocationSharingService.class);
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 64ef6dc6f50903ff9b0260d9ab70e8fc8298433e..f2b3eef53efeabf03585e6c72f5f6151ecd4c77b 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
@@ -49,6 +49,7 @@ import androidx.core.app.RemoteInput;
 import androidx.core.content.ContextCompat;
 import androidx.core.content.res.ResourcesCompat;
 import androidx.core.graphics.drawable.IconCompat;
+import androidx.core.util.Pair;
 
 import com.bumptech.glide.Glide;
 
@@ -339,7 +340,7 @@ public class NotificationServiceImpl implements NotificationService {
 
     @Override
     public void showLocationNotification(Account first, CallContact contact) {
-        android.net.Uri path = ConversationPath.toUri(first.getAccountID(), contact.getPrimaryUri());
+        android.net.Uri path = ConversationPath.toUri(first.getAccountID(), contact.getUri());
 
         Intent intentConversation = new Intent(Intent.ACTION_VIEW, path, mContext, ConversationActivity.class)
                 .putExtra(ConversationFragment.EXTRA_SHOW_MAP, true);
@@ -360,7 +361,7 @@ public class NotificationServiceImpl implements NotificationService {
 
     @Override
     public void cancelLocationNotification(Account first, CallContact contact) {
-        notificationManager.cancel(Objects.hash( "Location", ConversationPath.toUri(first.getAccountID(), contact.getPrimaryUri())));
+        notificationManager.cancel(Objects.hash( "Location", ConversationPath.toUri(first.getAccountID(), contact.getUri())));
     }
 
     /**
@@ -452,17 +453,17 @@ public class NotificationServiceImpl implements NotificationService {
      * Handles the creation and destruction of services associated with transfers as well as displaying notifications.
      *
      * @param transfer the data transfer object
-     * @param contact  the contact to whom the data transfer is being sent
+     * @param conversation  the contact to whom the data transfer is being sent
      * @param remove   true if it should be removed from current calls
      */
     @Override
-    public void handleDataTransferNotification(DataTransfer transfer, CallContact contact, boolean remove) {
+    public void handleDataTransferNotification(DataTransfer transfer, Conversation conversation, boolean remove) {
         Log.d(TAG, "handleDataTransferNotification, a data transfer event is in progress");
         if (DeviceUtils.isTv(mContext)) {
             return;
         }
         if (!remove) {
-            showFileTransferNotification(transfer, contact);
+            showFileTransferNotification(conversation, transfer);
         } else {
             removeTransferNotification(transfer.getDaemonId());
         }
@@ -498,25 +499,27 @@ public class NotificationServiceImpl implements NotificationService {
     public void showTextNotification(String accountId, Conversation conversation) {
         TreeMap<Long, TextMessage> texts = conversation.getUnreadTextMessages();
 
-        CallContact contact = conversation.getContact();
+        Log.w(TAG, "showTextNotification start " + accountId + " " + conversation.getUri() + " " + texts.size());
+
+        //TODO handle groups
         if (texts.isEmpty() || conversation.isVisible()) {
-            cancelTextNotification(contact.getPrimaryUri());
+            cancelTextNotification(conversation.getUri());
             return;
         }
         if (texts.lastEntry().getValue().isNotified()) {
             return;
         }
 
-        Log.w(TAG, "showTextNotification " + accountId + " " + contact.getPrimaryNumber());
-        mContactService.getLoadedContact(accountId, contact)
-                .subscribe(c -> textNotification(accountId, texts, c),
+        Log.w(TAG, "showTextNotification " + accountId + " " + conversation.getUri());
+        mContactService.getLoadedContact(accountId, conversation.getContacts(), false)
+                .subscribe(c -> textNotification(accountId, texts, conversation),
                         e -> Log.w(TAG, "Can't load contact", e));
     }
 
-    private void textNotification(String accountId, TreeMap<Long, TextMessage> texts, CallContact contact) {
-        Uri contactUri = contact.getPrimaryUri();
-        String contactId = contactUri.getUri();
-        android.net.Uri path = ConversationPath.toUri(accountId, contactId);
+    private void textNotification(String accountId, TreeMap<Long, TextMessage> texts, Conversation conversation) {
+        android.net.Uri path = ConversationPath.toUri(conversation.getAccountId(), conversation.getUri());
+        Pair<Bitmap, String> conversationProfile = getProfile(conversation);
+
         int notificationVisibility = mPreferencesService.getSettings().getNotificationVisibility();
         switch (notificationVisibility){
             case SettingsFragment.NOTIFICATION_PUBLIC:
@@ -530,11 +533,6 @@ public class NotificationServiceImpl implements NotificationService {
                 notificationVisibility = Notification.VISIBILITY_PRIVATE;
         }
 
-        String contactName = contact.getDisplayName();
-        if (TextUtils.isEmpty(contactName) || texts.isEmpty())
-            return;
-        Bitmap contactPicture = getContactPicture(contact);
-
         TextMessage last = texts.lastEntry().getValue();
         Intent intentConversation = new Intent(DRingService.ACTION_CONV_ACCEPT, path, mContext, DRingService.class);
         Intent intentDelete = new Intent(DRingService.ACTION_CONV_DISMISS, path, mContext, DRingService.class);
@@ -545,7 +543,7 @@ public class NotificationServiceImpl implements NotificationService {
                 .setDefaults(NotificationCompat.DEFAULT_ALL)
                 .setVisibility(notificationVisibility)
                 .setSmallIcon(R.drawable.ic_ring_logo_white)
-                .setContentTitle(contactName)
+                .setContentTitle(conversationProfile.second)
                 .setContentText(last.getBody())
                 .setWhen(last.getTimestamp())
                 .setContentIntent(PendingIntent.getService(mContext, random.nextInt(), intentConversation, 0))
@@ -553,28 +551,28 @@ public class NotificationServiceImpl implements NotificationService {
                 .setAutoCancel(true)
                 .setColor(ResourcesCompat.getColor(mContext.getResources(), R.color.color_primary_dark, null));
 
-        String key = ConversationPath.toKey(accountId, contactId);
+        String key = ConversationPath.toKey(accountId, conversation.getUri());
 
         Person contactPerson = new Person.Builder()
                 .setKey(key)
-                .setName(contactName)
-                .setIcon(contactPicture == null ? null : IconCompat.createWithBitmap(contactPicture))
+                .setName(conversationProfile.second)
+                .setIcon(conversationProfile.first == null ? null : IconCompat.createWithBitmap(conversationProfile.first))
                 .build();
 
-        if (contactPicture != null) {
-            messageNotificationBuilder.setLargeIcon(contactPicture);
+        if (conversationProfile.first != null) {
+            messageNotificationBuilder.setLargeIcon(conversationProfile.first);
             Intent intentBubble = new Intent(Intent.ACTION_VIEW, path, mContext, ConversationActivity.class);
             intentBubble.putExtra(EXTRA_BUBBLE, true);
             messageNotificationBuilder.setBubbleMetadata(new NotificationCompat.BubbleMetadata.Builder()
                     .setDesiredHeight(600)
-                    .setIcon(IconCompat.createWithAdaptiveBitmap(contactPicture))
+                    .setIcon(IconCompat.createWithAdaptiveBitmap(conversationProfile.first))
                     .setIntent(PendingIntent.getActivity(mContext, 0, intentBubble,
                             PendingIntent.FLAG_UPDATE_CURRENT))
                     .build())
                     .setShortcutId(key);
         }
 
-        UnreadConversation.Builder unreadConvBuilder = new UnreadConversation.Builder(contactName)
+        UnreadConversation.Builder unreadConvBuilder = new UnreadConversation.Builder(conversationProfile.second)
                 .setLatestTimestamp(last.getTimestamp());
 
         if (texts.size() == 1) {
@@ -593,6 +591,13 @@ public class NotificationServiceImpl implements NotificationService {
 
             NotificationCompat.MessagingStyle history = new NotificationCompat.MessagingStyle(userPerson);
             for (TextMessage textMessage : texts.values()) {
+                CallContact contact = textMessage.getContact();
+                Bitmap contactPicture = getContactPicture(contact);
+                Person contactPerson = new Person.Builder()
+                        .setKey(textMessage.getAuthor())
+                        .setName(contact.getDisplayName())
+                        .setIcon(contactPicture == null ? null : IconCompat.createWithBitmap(contactPicture))
+                        .build();
                 history.addMessage(new NotificationCompat.MessagingStyle.Message(
                         textMessage.getBody(),
                         textMessage.getTimestamp(),
@@ -602,7 +607,7 @@ public class NotificationServiceImpl implements NotificationService {
             messageNotificationBuilder.setStyle(history);
         }
 
-        int notificationId = getTextNotificationId(contactUri);
+        int notificationId = getTextNotificationId(conversation.getUri());
         int replyId = notificationId + 1;
         int markAsReadId = notificationId + 2;
 
@@ -670,38 +675,31 @@ public class NotificationServiceImpl implements NotificationService {
             return;
         if (requests.size() == 1) {
             Conversation request = requests.iterator().next();
-            CallContact contact = request.getContact();
-            String contactKey = contact.getPrimaryUri().getRawRingId();
+            String contactKey = request.getUri().getRawUriString();
             if (notifiedRequests.contains(contactKey)) {
                 return;
             }
-            mContactService.getLoadedContact(account.getAccountID(), contact).subscribe(c -> {
+            mContactService.getLoadedContact(account.getAccountID(), request.getContacts(), false).subscribe(c -> {
                 NotificationCompat.Builder builder = getRequestNotificationBuilder(account.getAccountID());
                 mPreferencesService.saveRequestPreferences(account.getAccountID(), contactKey);
-                Bundle info = new Bundle();
-                info.putString(TRUST_REQUEST_NOTIFICATION_ACCOUNT_ID, account.getAccountID());
-                info.putString(TRUST_REQUEST_NOTIFICATION_FROM, c.getPrimaryNumber());
-                builder.setContentText(c.getRingUsername())
+                android.net.Uri info = ConversationPath.toUri(account.getAccountID(), request.getUri());
+                builder.setContentText(request.getUriTitle())
                         .addAction(R.drawable.baseline_person_add_24, mContext.getText(R.string.accept),
                                 PendingIntent.getService(mContext, random.nextInt(),
-                                        new Intent(DRingService.ACTION_TRUST_REQUEST_ACCEPT)
-                                                .setClass(mContext, DRingService.class)
-                                                .putExtras(info),
+                                        new Intent(DRingService.ACTION_TRUST_REQUEST_ACCEPT, info, mContext, DRingService.class),
                                         PendingIntent.FLAG_ONE_SHOT))
                         .addAction(R.drawable.baseline_delete_24, mContext.getText(R.string.refuse),
                                 PendingIntent.getService(mContext, random.nextInt(),
-                                        new Intent(DRingService.ACTION_TRUST_REQUEST_REFUSE)
-                                                .setClass(mContext, DRingService.class)
-                                                .putExtras(info),
+                                        new Intent(DRingService.ACTION_TRUST_REQUEST_REFUSE, info, mContext, DRingService.class),
                                         PendingIntent.FLAG_ONE_SHOT))
                         .addAction(R.drawable.baseline_block_24, mContext.getText(R.string.block),
                                 PendingIntent.getService(mContext, random.nextInt(),
-                                        new Intent(DRingService.ACTION_TRUST_REQUEST_BLOCK)
-                                                .setClass(mContext, DRingService.class)
-                                                .putExtras(info),
+                                        new Intent(DRingService.ACTION_TRUST_REQUEST_BLOCK, info, mContext, DRingService.class),
                                         PendingIntent.FLAG_ONE_SHOT));
 
-                setContactPicture(c, builder);
+                Bitmap pic = getContactPicture(request);
+                if (pic != null)
+                    builder.setLargeIcon(pic);
                 notificationManager.notify(notificationId, builder.build());
             }, e -> Log.w(TAG, "error showing notification", e));
         } else {
@@ -709,10 +707,12 @@ public class NotificationServiceImpl implements NotificationService {
             boolean newRequest = false;
             for (Conversation request : requests) {
                 CallContact contact = request.getContact();
-                String contactKey = contact.getPrimaryUri().getRawRingId();
-                if (!notifiedRequests.contains(contactKey)) {
-                    newRequest = true;
-                    mPreferencesService.saveRequestPreferences(account.getAccountID(), contactKey);
+                if (contact != null) {
+                    String contactKey = contact.getUri().getRawRingId();
+                    if (!notifiedRequests.contains(contactKey)) {
+                        newRequest = true;
+                        mPreferencesService.saveRequestPreferences(account.getAccountID(), contactKey);
+                    }
                 }
             }
             if (!newRequest)
@@ -724,7 +724,7 @@ public class NotificationServiceImpl implements NotificationService {
     }
 
     @Override
-    public void showFileTransferNotification(DataTransfer info, CallContact contact) {
+    public void showFileTransferNotification(Conversation conversation, DataTransfer info) {
         if (info == null) {
             return;
         }
@@ -735,7 +735,7 @@ public class NotificationServiceImpl implements NotificationService {
         long dataTransferId = info.getDaemonId();
         int notificationId = getFileTransferNotificationId(dataTransferId);
 
-        android.net.Uri path = ConversationPath.toUri(info.getAccount(), new Uri(info.getConversation().getParticipant()));
+        android.net.Uri path = ConversationPath.toUri(info.getAccount(), conversation.getUri());
 
         Intent intentConversation = new Intent(DRingService.ACTION_CONV_ACCEPT, path, mContext, DRingService.class);
 
@@ -752,7 +752,7 @@ public class NotificationServiceImpl implements NotificationService {
                     .setAutoCancel(true);
 
             if (info.showPicture()) {
-                File filePath = mDeviceRuntimeService.getConversationPath(info.getPeerId(), info.getStoragePath());
+                File filePath = mDeviceRuntimeService.getConversationPath(conversation.getUri().getRawRingId(), info.getStoragePath());
                 Bitmap img;
                 try {
                     BitmapDrawable d = (BitmapDrawable) Glide.with(mContext)
@@ -760,7 +760,7 @@ public class NotificationServiceImpl implements NotificationService {
                             .submit()
                             .get();
                     img = d.getBitmap();
-                    notif.setContentTitle(mContext.getString(R.string.notif_incoming_picture, contact.getDisplayName()));
+                    notif.setContentTitle(mContext.getString(R.string.notif_incoming_picture, conversation.getTitle()));
                     notif.setStyle(new NotificationCompat.BigPictureStyle()
                             .bigPicture(img));
                 } catch (Exception e) {
@@ -768,11 +768,12 @@ public class NotificationServiceImpl implements NotificationService {
                     return;
                 }
             } else {
-                notif.setContentTitle(mContext.getString(R.string.notif_incoming_file_transfer_title, contact.getDisplayName()));
+                notif.setContentTitle(mContext.getString(R.string.notif_incoming_file_transfer_title, conversation.getTitle()));
                 notif.setStyle(null);
             }
-
-            setContactPicture(contact, notif);
+            Bitmap picture = getContactPicture(conversation);
+            if (picture != null)
+                notif.setLargeIcon(picture);
             notificationManager.notify(random.nextInt(), notif.build());
             return;
         }
@@ -782,7 +783,7 @@ public class NotificationServiceImpl implements NotificationService {
         }
 
         boolean ongoing = event == InteractionStatus.TRANSFER_ONGOING || event == InteractionStatus.TRANSFER_ACCEPTED;
-        String titleMessage = mContext.getString(info.isOutgoing() ? R.string.notif_outgoing_file_transfer_title : R.string.notif_incoming_file_transfer_title, contact.getDisplayName());
+        String titleMessage = mContext.getString(info.isOutgoing() ? R.string.notif_outgoing_file_transfer_title : R.string.notif_incoming_file_transfer_title, conversation.getTitle());
         messageNotificationBuilder.setContentTitle(titleMessage)
                 .setPriority(NotificationCompat.PRIORITY_DEFAULT)
                 .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
@@ -796,7 +797,9 @@ public class NotificationServiceImpl implements NotificationService {
                         info.getDisplayName() + ": " + ResourceMapper.getReadableFileTransferStatus(mContext, event))
                 .setContentIntent(PendingIntent.getService(mContext, random.nextInt(), intentConversation, 0))
                 .setColor(ResourcesCompat.getColor(mContext.getResources(), R.color.color_primary_dark, null));
-        setContactPicture(contact, messageNotificationBuilder);
+        Bitmap picture = getContactPicture(conversation);
+        if (picture != null)
+            messageNotificationBuilder.setLargeIcon(picture);
         if (event.isOver()) {
             messageNotificationBuilder.setProgress(0, 0, false);
         } else if (ongoing) {
@@ -817,14 +820,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)
-                                            .setClass(mContext, DRingService.class)
+                                    new Intent(DRingService.ACTION_FILE_ACCEPT, ConversationPath.toUri(conversation), 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)
-                                            .setClass(mContext, DRingService.class)
+                                    new Intent(DRingService.ACTION_FILE_CANCEL, ConversationPath.toUri(conversation), mContext, DRingService.class)
                                             .putExtra(DRingService.KEY_TRANSFER_ID, dataTransferId),
                                     PendingIntent.FLAG_ONE_SHOT));
             mNotificationBuilders.put(notificationId, messageNotificationBuilder);
@@ -840,7 +841,6 @@ public class NotificationServiceImpl implements NotificationService {
                                     PendingIntent.FLAG_ONE_SHOT));
         }
         mNotificationBuilders.put(notificationId, messageNotificationBuilder);
-        dataTransferNotifications.remove(notificationId);
         dataTransferNotifications.put(notificationId, messageNotificationBuilder.build());
         startForegroundService(notificationId, DataTransferService.class);
     }
@@ -901,8 +901,8 @@ public class NotificationServiceImpl implements NotificationService {
         mNotificationBuilders.remove(notificationId);
     }
 
-    public void cancelTextNotification(String ringId) {
-        int notificationId = (NOTIF_MSG + ringId).hashCode();
+    public void cancelTextNotification(String accountId, Uri contact) {
+        int notificationId = getTextNotificationId(contact);
         notificationManager.cancel(notificationId);
         mNotificationBuilders.remove(notificationId);
     }
@@ -954,7 +954,14 @@ public class NotificationServiceImpl implements NotificationService {
 
     private Bitmap getContactPicture(CallContact contact) {
         try {
-            return AvatarFactory.getBitmapAvatar(mContext, contact, avatarSize).blockingGet();
+            return AvatarFactory.getBitmapAvatar(mContext, contact, avatarSize, false).blockingGet();
+        } catch (Exception e) {
+            return null;
+        }
+    }
+    private Bitmap getContactPicture(Conversation conversation) {
+        try {
+            return AvatarFactory.getBitmapAvatar(mContext, conversation, avatarSize, false).blockingGet();
         } catch (Exception e) {
             return null;
         }
@@ -964,6 +971,10 @@ public class NotificationServiceImpl implements NotificationService {
         return AvatarFactory.getBitmapAvatar(mContext, account, avatarSize).blockingGet();
     }
 
+    private Pair<Bitmap, String> getProfile(Conversation conversation) {
+        return Pair.create(getContactPicture(conversation), conversation.getTitle());
+    }
+
     private void setContactPicture(CallContact contact, NotificationCompat.Builder messageNotificationBuilder) {
         Bitmap pic = getContactPicture(contact);
         if (pic != null)
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 e0a875323cf9d19c7cef4d36d45a1841c6a761da..afc22c712d54eb249d5d6a1a47bd077734fa2c0f 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
@@ -27,13 +27,9 @@ import androidx.annotation.NonNull;
 
 import java.io.ByteArrayOutputStream;
 import java.io.File;
-import java.util.List;
-import java.util.Map;
 
 import cx.ring.model.Account;
-import cx.ring.model.CallContact;
 import cx.ring.utils.BitmapUtils;
-import cx.ring.utils.FileUtils;
 import cx.ring.utils.Tuple;
 import cx.ring.utils.VCardUtils;
 import ezvcard.VCard;
@@ -110,94 +106,6 @@ public class VCardServiceImpl extends VCardService {
         return new Tuple<>(profile.first, BitmapUtils.bytesToBitmap(profile.second));
     }
 
-    /**
-     * Migrates the user's contacts to their individual account folders under the subfolder profiles
-     * @param contacts a hash map of the user's contacts
-     * @param accountId the directory where the profile is stored
-     */
-    @Override
-    public void migrateContact(Map<String, CallContact> contacts, String accountId) {
-        File fileDir = mContext.getFilesDir();
-        File legacyProfileFolder = new File(fileDir, "peer_profiles");
-        if (!legacyProfileFolder.exists())
-            return;
-
-        File[] profiles = legacyProfileFolder.listFiles();
-
-        if (profiles == null)
-            return;
-
-        File accountDir = new File(fileDir, accountId);
-        File profilesDir = new File(accountDir, "profiles");
-        profilesDir.mkdirs();
-
-
-        for (File profile : profiles) {
-            String filename = profile.getName();
-            String contactUri = filename.lastIndexOf(".") > 0 ? filename.substring(0, filename.lastIndexOf(".")) : filename;
-            if (contacts.containsKey(contactUri)) {
-                File destination = new File(profilesDir, filename);
-                FileUtils.copyFile(profile, destination);
-            }
-        }
-    }
-
-    /**
-     * Migrates the user's vcards and renames them to profile.vcf
-     *
-     * @param accountIds the list of accounts to migrate
-     */
-    @Override
-    public void migrateProfiles(List<String> accountIds) {
-        File fileDir = mContext.getFilesDir();
-        File profileDir = new File(fileDir, "profiles");
-
-        if (!profileDir.exists())
-            return;
-
-        File[] profiles = profileDir.listFiles();
-
-        if (profiles == null)
-            return;
-
-        for (File profile : profiles) {
-            for (String account : accountIds) {
-                if (profile.getName().equals(account + ".vcf")) {
-                    File accountDir = new File(fileDir, account);
-                    File newProfile = new File(accountDir, VCardUtils.LOCAL_USER_VCARD_NAME);
-                    FileUtils.moveFile(profile, newProfile);
-                    break;
-                }
-            }
-        }
-
-        // necessary to delete profiles leftover by deleted accounts
-        for (File profile : profiles) {
-            profile.delete();
-        }
-
-
-        profileDir.delete();
-    }
-
-    /**
-     * Deletes the legacy peer_profiles folder
-     */
-    @Override
-    public void deleteLegacyProfiles() {
-        File fileDir = mContext.getFilesDir();
-        File legacyProfileFolder = new File(fileDir, "peer_profiles");
-        File[] profiles = legacyProfileFolder.listFiles();
-        if (profiles == null)
-            return;
-
-        for (File file : profiles) {
-            file.delete();
-        }
-
-        legacyProfileFolder.delete();
-    }
-
     @Override
     public Object base64ToBitmap(String base64) {
         return BitmapUtils.base64ToBitmap(base64);
diff --git a/ring-android/app/src/main/java/cx/ring/share/ScanFragment.java b/ring-android/app/src/main/java/cx/ring/share/ScanFragment.java
index 9b3325dd9d05b5154791086e1e48457d5ee9df08..519c21f9b78425cf94769ab8a19cde13c5fa35ff 100644
--- a/ring-android/app/src/main/java/cx/ring/share/ScanFragment.java
+++ b/ring-android/app/src/main/java/cx/ring/share/ScanFragment.java
@@ -52,15 +52,12 @@ import cx.ring.R;
 import cx.ring.application.JamiApplication;
 import cx.ring.client.HomeActivity;
 import cx.ring.fragments.ConversationFragment;
+import cx.ring.fragments.QRCodeFragment;
 import cx.ring.mvp.BaseSupportFragment;
 
 public class ScanFragment extends BaseSupportFragment {
-
     public static final String TAG = ScanFragment.class.getSimpleName();
 
-    private boolean mIsVisible;
-    private boolean mIsStarted;
-
     private DecoratedBarcodeView barcodeView;
     private TextView mErrorMessageTextView;
 
@@ -99,20 +96,6 @@ public class ScanFragment extends BaseSupportFragment {
         }
     }
 
-    @Override
-    public void onStart() {
-        super.onStart();
-        mIsStarted = true;
-        if (mIsVisible)
-            checkPermission();
-    }
-
-    @Override
-    public void onStop() {
-        super.onStop();
-        mIsStarted = false;
-    }
-
     private void showErrorPanel(final int textResId) {
         if (mErrorMessageTextView != null) {
             mErrorMessageTextView.setText(textResId);
@@ -157,9 +140,8 @@ public class ScanFragment extends BaseSupportFragment {
     }
     private void initializeBarcode() {
         if (barcodeView != null) {
-            Collection<BarcodeFormat> formats = Collections.singletonList(BarcodeFormat.QR_CODE);
-            barcodeView.getBarcodeView().setDecoderFactory(new DefaultDecoderFactory(formats));
-            barcodeView.initializeFromIntent(getActivity().getIntent());
+            barcodeView.getBarcodeView().setDecoderFactory(new DefaultDecoderFactory(Collections.singletonList(BarcodeFormat.QR_CODE)));
+            //barcodeView.initializeFromIntent(getActivity().getIntent());
             barcodeView.decodeContinuous(callback);
         }
     }
@@ -168,9 +150,13 @@ public class ScanFragment extends BaseSupportFragment {
         @Override
         public void barcodeResult(@NonNull BarcodeResult result) {
             if (result.getText() != null) {
-                String contact_uri = result.getText();
-                if (contact_uri != null ) {
-                    goToConversation(contact_uri);
+                String contactUri = result.getText();
+                if (contactUri != null) {
+                    QRCodeFragment parent = (QRCodeFragment) getParentFragment();
+                    if (parent != null) {
+                        parent.dismiss();
+                    }
+                    goToConversation(contactUri);
                 }
             }
         }
@@ -180,8 +166,8 @@ public class ScanFragment extends BaseSupportFragment {
         }
     };
 
-    private void goToConversation(String contactId) {
-        ((HomeActivity) requireActivity()).startConversation(contactId);
+    private void goToConversation(String conversationUri) {
+        ((HomeActivity) requireActivity()).startConversation(conversationUri);
     }
 
     private boolean checkPermission() {
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 17a76af6ce3ea82bbe9b977970367ec53b133548..90c20b9f4b0be1bd5e8f5c6c1971d2819dfa6317 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
@@ -323,7 +323,7 @@ public class TVCallFragment extends BaseSupportFragment<CallPresenter> implement
             if (resultCode == Activity.RESULT_OK && data != null) {
                 ConversationPath path = ConversationPath.fromUri(data.getData());
                 if (path != null) {
-                    presenter.addConferenceParticipant(path.getAccountId(), path.getContactId());
+                    presenter.addConferenceParticipant(path.getAccountId(), path.getConversationUri());
                 }
             }
         }
@@ -651,9 +651,11 @@ public class TVCallFragment extends BaseSupportFragment<CallPresenter> implement
             Bundle args;
             args = getArguments();
             if (args != null) {
-                presenter.initOutGoing(getArguments().getString(KEY_ACCOUNT_ID),
-                        getArguments().getString(KEY_CONTACT_RING_ID),
-                        getArguments().getBoolean(KEY_AUDIO_ONLY));
+                ConversationPath conversation = ConversationPath.fromBundle(args);
+                presenter.initOutGoing(conversation.getAccountId(),
+                        conversation.getConversationUri(),
+                        args.getString(Intent.EXTRA_PHONE_NUMBER),
+                        args.getBoolean(KEY_AUDIO_ONLY));
             }
         }
     }
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 5e95306e5549daaa066e81b96724ba2ba45e5f4e..f85cfdaae2c2591ab8bec4b5c556281d5574b7a7 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
@@ -40,10 +40,10 @@ public class ContactCard extends Card {
 
     public void setModel(SmartListViewModel model) {
         mModel = model;
-        setTitle(mModel.getContact().getDisplayName());
-        String username = mModel.getContact().getRingUsername();
+        setTitle(mModel.getContactName());
+        String username = mModel.getContact().get(0).getRingUsername();
         setDescription(username);
-        boolean isOnline = mModel.getContact().isOnline();
+        boolean isOnline = mModel.getContact().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/cards/contacts/ContactCardPresenter.java b/ring-android/app/src/main/java/cx/ring/tv/cards/contacts/ContactCardPresenter.java
index b9995a01373e6904b9f5ab3639660718023de30c..f0798ae0f5bfa139240b0488fec12fbcc53d65c5 100644
--- a/ring-android/app/src/main/java/cx/ring/tv/cards/contacts/ContactCardPresenter.java
+++ b/ring-android/app/src/main/java/cx/ring/tv/cards/contacts/ContactCardPresenter.java
@@ -74,7 +74,7 @@ public class ContactCardPresenter extends AbstractCardPresenter<ImageCardView> {
         cardView.setBackgroundColor(cardView.getResources().getColor(R.color.color_primary_dark));
         cardView.setMainImage(
                 new AvatarDrawable.Builder()
-                        .withContact(model.getContact())
+                        .withViewModel(model)
                         .withPresence(false)
                         .withCircleCrop(false)
                         .build(getContext())
diff --git a/ring-android/app/src/main/java/cx/ring/tv/contact/TVContactFragment.java b/ring-android/app/src/main/java/cx/ring/tv/contact/TVContactFragment.java
index c42bf43e45ac1c6c78248f3fa14feda33dd1a25a..5b75bac06d3b9bade0e3af2cc8413c9c13e0ffd6 100644
--- a/ring-android/app/src/main/java/cx/ring/tv/contact/TVContactFragment.java
+++ b/ring-android/app/src/main/java/cx/ring/tv/contact/TVContactFragment.java
@@ -168,8 +168,8 @@ public class TVContactFragment extends BaseDetailFragment<TVContactPresenter> im
         final DetailsOverviewRow row = new DetailsOverviewRow(model);
         AvatarDrawable avatar =
                 new AvatarDrawable.Builder()
-                        .withContact(model.getContact())
-                        .withPresence(false)
+                        .withViewModel(model)
+                        //.withPresence(false)
                         .withCircleCrop(false)
                         .build(getActivity());
         avatar.setInSize(iconSize);
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 55493821f4b52f344852855faac13f12d818270c..a3734c74bbaf636108e63d5d02a7f21b83cf11c2 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
@@ -26,6 +26,7 @@ import cx.ring.daemon.Blob;
 import cx.ring.facades.ConversationFacade;
 import cx.ring.model.Account;
 import cx.ring.model.Conference;
+import cx.ring.model.Conversation;
 import cx.ring.model.SipCall;
 import cx.ring.model.Uri;
 import cx.ring.mvp.RootPresenter;
@@ -98,9 +99,10 @@ 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)
-                .subscribe(vCard -> mAccountService.sendTrustRequest(accountId, conversationUri.getRawRingId(), Blob.fromString(VCardUtils.vcardToString(vCard))),
-                        e -> mAccountService.sendTrustRequest(accountId, conversationUri.getRawRingId(), null));
+                .subscribe(vCard -> mAccountService.sendTrustRequest(conversation, conversationUri, Blob.fromString(VCardUtils.vcardToString(vCard))),
+                        e -> mAccountService.sendTrustRequest(conversation, conversationUri, null));
     }
 
     public void acceptTrustRequest() {
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 213757da530586b896bdd2db6c0755c633303366..f797751982263ec50925c09ec07e3d494cdb6262 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().getRingUsername();
+            String id = viewModel.getContact().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 4b1f3206f7529e298b279acf9987f304bfcfab4e..4e968ad08a721097d50f1dd42aac1e939bedd471 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
@@ -365,7 +365,7 @@ public class TvConversationAdapter extends RecyclerView.Adapter<TvConversationVi
     private void configureForFileInfoTextMessage(@NonNull final TvConversationViewHolder viewHolder,
                                                  @NonNull final Interaction interaction, int position) {
         DataTransfer file = (DataTransfer) interaction;
-        File path = presenter.getDeviceRuntimeService().getConversationPath(file.getPeerId(), file.getStoragePath());
+        File path = presenter.getDeviceRuntimeService().getConversationPath(file.getConversationId(), file.getStoragePath());
         file.setSize(path.length());
 
         String timeString = timestampToDetailString(viewHolder.itemView.getContext(), file.getTimestamp());
diff --git a/ring-android/app/src/main/java/cx/ring/tv/conversation/TvConversationFragment.java b/ring-android/app/src/main/java/cx/ring/tv/conversation/TvConversationFragment.java
index 8552988ee26d483d5de7769227aef878ce095982..ee7b2b6e8d1ca155bfe7bc56ef871d825fe0f002 100644
--- a/ring-android/app/src/main/java/cx/ring/tv/conversation/TvConversationFragment.java
+++ b/ring-android/app/src/main/java/cx/ring/tv/conversation/TvConversationFragment.java
@@ -74,6 +74,7 @@ import cx.ring.model.DataTransfer;
 import cx.ring.model.Error;
 import cx.ring.model.Interaction;
 import cx.ring.mvp.BaseSupportFragment;
+import cx.ring.service.DRingService;
 import cx.ring.tv.camera.CustomCameraActivity;
 import cx.ring.utils.AndroidFileUtils;
 import cx.ring.utils.ContentUriHandler;
@@ -605,15 +606,16 @@ public class TvConversationFragment extends BaseSupportFragment<ConversationPres
     }
 
     @Override
-    public void displayContact(CallContact contact) {
+    public void displayContact(Conversation conversation) {
+        List<CallContact> contacts = conversation.getContacts();
         mCompositeDisposable.clear();
-        mCompositeDisposable.add(AvatarFactory.getAvatar(requireContext(), contact)
+        mCompositeDisposable.add(AvatarFactory.getAvatar(requireContext(), conversation, true)
                 .doOnSuccess(d -> {
                     mConversationAvatar = (AvatarDrawable) d;
-                    mParticipantAvatars.put(contact.getPrimaryNumber(),
+                    mParticipantAvatars.put(contacts.get(0).getPrimaryNumber(),
                             new AvatarDrawable((AvatarDrawable) d));
                 })
-                .flatMapObservable(d -> contact.getUpdatesSubject())
+                .flatMapObservable(d -> contacts.get(0).getUpdatesSubject())
                 .observeOn(AndroidSchedulers.mainThread())
                 .subscribe(c -> {
                     String id = c.getRingUsername();
@@ -624,7 +626,7 @@ public class TvConversationFragment extends BaseSupportFragment<ConversationPres
                     else
                         binding.subtitle.setVisibility(View.GONE);
                     mConversationAvatar.update(c);
-                    String uri = contact.getPrimaryNumber();
+                    String uri = contacts.get(0).getPrimaryNumber();
                     AvatarDrawable a = mParticipantAvatars.get(uri);
                     if (a != null)
                         a.update(c);
@@ -655,6 +657,16 @@ public class TvConversationFragment extends BaseSupportFragment<ConversationPres
 
     }
 
+    @Override
+    public void updateContact(CallContact contact) {
+        mCompositeDisposable.add(AvatarFactory.getAvatar(requireContext(), contact, true)
+                .observeOn(AndroidSchedulers.mainThread())
+                .subscribe(avatar -> {
+                    mParticipantAvatars.put(contact.getPrimaryNumber(), (AvatarDrawable) avatar);
+                    mAdapter.setPhoto();
+                }));
+    }
+
     @Override
     public void setComposingStatus(Account.ComposingStatus composingStatus) {
 
@@ -709,6 +721,11 @@ public class TvConversationFragment extends BaseSupportFragment<ConversationPres
 
     }
 
+    @Override
+    public void updateLastRead(String last) {
+
+    }
+
     @Override
     public void displayOnGoingCallPane(boolean display) {
 
@@ -745,12 +762,12 @@ public class TvConversationFragment extends BaseSupportFragment<ConversationPres
     }
 
     @Override
-    public void goToCallActivityWithResult(String accountId, String contactRingId, boolean audioOnly) {
+    public void goToCallActivityWithResult(String accountId, cx.ring.model.Uri conversationUri, cx.ring.model.Uri contactRingId, boolean audioOnly) {
 
     }
 
     @Override
-    public void goToContactActivity(String accountId, String contactRingId) {
+    public void goToContactActivity(String accountId, cx.ring.model.Uri contactRingId) {
 
     }
 
@@ -774,4 +791,26 @@ public class TvConversationFragment extends BaseSupportFragment<ConversationPres
 
     }
 
+    @Override
+    public void acceptFile(String accountId, net.jami.model.Uri conversationUri, DataTransfer transfer) {
+        File cacheDir = requireContext().getCacheDir();
+        long spaceLeft = AndroidFileUtils.getSpaceLeft(cacheDir.toString());
+        if (spaceLeft == -1L || transfer.getTotalSize() > spaceLeft) {
+            presenter.noSpaceLeft();
+            return;
+        }
+        requireActivity().startService(new Intent(DRingService.ACTION_FILE_ACCEPT)
+                .setClass(requireContext(), DRingService.class)
+                .setData(ConversationPath.toUri(accountId, conversationUri))
+                .putExtra(DRingService.KEY_TRANSFER_ID, transfer.getDaemonId()));
+    }
+
+    @Override
+    public void refuseFile(String accountId, net.jami.model.Uri conversationUri, DataTransfer transfer) {
+        requireActivity().startService(new Intent(DRingService.ACTION_FILE_CANCEL)
+                .setClass(requireContext(), DRingService.class)
+                .setData(ConversationPath.toUri(accountId, conversationUri))
+                .putExtra(DRingService.KEY_TRANSFER_ID, transfer.getDaemonId()));
+    }
+
 }
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 edcb09ff98480fe1aa4973275dd2f990e3f6934e..63cd7f69cd38e4b15ac5504ac5eb8ce5e5a54375 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
@@ -282,10 +282,8 @@ public class MainFragment extends BaseBrowseFragment<MainPresenter> implements M
 
     private static Single<PreviewProgram> buildProgram(Context context, SmartListViewModel vm, String launcherName, long channelId) {
         return new AvatarDrawable.Builder()
-                .withContact(vm.getContact())
+                .withViewModel(vm)
                 .withPresence(false)
-                //.withPresence(vm.getContact().size() == 1)
-                //.withOnlineState(vm.getContact().size() == 1 && vm.getContact().get(0).isOnline())
                 .buildAsync(context)
                 .map(avatar -> {
                     File file = AndroidFileUtils.createImageFile(context);
@@ -304,7 +302,7 @@ public class MainFragment extends BaseBrowseFragment<MainPresenter> implements M
                             .setChannelId(channelId)
                             .setType(TvContractCompat.PreviewPrograms.TYPE_CLIP)
                             .setTitle(vm.getContactName())
-                            .setAuthor(vm.getContact().getRingUsername())
+                            .setAuthor(vm.getContact().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/tv/search/ContactSearchFragment.java b/ring-android/app/src/main/java/cx/ring/tv/search/ContactSearchFragment.java
index 6faae15edb9bec675198e99bcccf98379b16c3f0..90a4440eb643e9a9e300288f7cdfaf49817c680d 100644
--- a/ring-android/app/src/main/java/cx/ring/tv/search/ContactSearchFragment.java
+++ b/ring-android/app/src/main/java/cx/ring/tv/search/ContactSearchFragment.java
@@ -139,8 +139,8 @@ public class ContactSearchFragment extends BaseSearchFragment<ContactSearchPrese
     public void startCall(String accountID, String number) {
         Intent intent = new Intent(CallActivity.ACTION_CALL, ConversationPath.toUri(accountID, number), getActivity(), TVCallActivity.class);
         intent.putExtra(ConversationFragment.KEY_ACCOUNT_ID, accountID);
-        intent.putExtra(ConversationFragment.KEY_CONTACT_RING_ID, number);
-        getActivity().startActivity(intent, null);
+        intent.putExtra(Intent.EXTRA_PHONE_NUMBER, number);
+        startActivity(intent);
         getActivity().finish();
     }
 
@@ -149,7 +149,7 @@ public class ContactSearchFragment extends BaseSearchFragment<ContactSearchPrese
         Intent intent = new Intent(getActivity(), TVContactActivity.class);
         //intent.putExtra(TVContactActivity.CONTACT_REQUEST_URI, model.getContact().getPrimaryUri());
         intent.setDataAndType(ConversationPath.toUri(model.getAccountId(), model.getUri()), TVContactActivity.TYPE_CONTACT_REQUEST_OUTGOING);
-        getActivity().startActivity(intent);
+        startActivity(intent);
         getActivity().finish();
     }
 }
diff --git a/ring-android/app/src/main/java/cx/ring/tv/search/ContactSearchPresenter.java b/ring-android/app/src/main/java/cx/ring/tv/search/ContactSearchPresenter.java
index b2b5a53378de49802d48c83bfc30ed829997d9c3..76d8723501a865aa437abebcba2a98ce3c003a88 100644
--- a/ring-android/app/src/main/java/cx/ring/tv/search/ContactSearchPresenter.java
+++ b/ring-android/app/src/main/java/cx/ring/tv/search/ContactSearchPresenter.java
@@ -82,8 +82,8 @@ public class ContactSearchPresenter extends RootPresenter<ContactSearchView> {
                 return;
             }
 
-            Uri uri = new Uri(query);
-            if (uri.isRingId()) {
+            Uri uri = Uri.fromString(query);
+            if (uri.isHexId()) {
                 mCallContact = currentAccount.getContactFromCache(uri);
                 getView().displayContact(currentAccount.getAccountID(), mCallContact);
             } else {
@@ -103,8 +103,8 @@ public class ContactSearchPresenter extends RootPresenter<ContactSearchView> {
                 break;
             case 1:
                 // invalid name
-                Uri uriName = new Uri(name);
-                if (uriName.isRingId()) {
+                Uri uriName = Uri.fromString(name);
+                if (uriName.isHexId()) {
                     mCallContact = account.getContactFromCache(uriName);
                     getView().displayContact(account.getAccountID(), mCallContact);
                 } else {
@@ -113,8 +113,8 @@ public class ContactSearchPresenter extends RootPresenter<ContactSearchView> {
                 break;
             default:
                 // on error
-                Uri uriAddress = new Uri(address);
-                if (uriAddress.isRingId()) {
+                Uri uriAddress = Uri.fromString(address);
+                if (uriAddress.isHexId()) {
                     mCallContact = account.getContactFromCache(uriAddress);
                     getView().displayContact(account.getAccountID(), mCallContact);
                 } else {
diff --git a/ring-android/app/src/main/java/cx/ring/utils/ActionHelper.java b/ring-android/app/src/main/java/cx/ring/utils/ActionHelper.java
index 9475b92da17ef7097d4bb7e2383522d2f377b0ed..9b02f1102c214c036f5484392e71460cc3d2f551 100644
--- a/ring-android/app/src/main/java/cx/ring/utils/ActionHelper.java
+++ b/ring-android/app/src/main/java/cx/ring/utils/ActionHelper.java
@@ -25,17 +25,13 @@ import android.content.Context;
 import android.content.Intent;
 import android.provider.ContactsContract;
 
-import androidx.appcompat.app.AlertDialog;
-
 import com.google.android.material.dialog.MaterialAlertDialogBuilder;
 
 import java.util.ArrayList;
 
 import cx.ring.R;
-import cx.ring.adapters.NumberAdapter;
 import cx.ring.model.CallContact;
 import cx.ring.model.Conversation;
-import cx.ring.model.Phone;
 import cx.ring.model.Uri;
 
 public class ActionHelper {
@@ -50,14 +46,14 @@ public class ActionHelper {
     }
 
     public static void launchClearAction(final Context context,
-                                                 final CallContact callContact,
+                                                 final Uri uri,
                                                  final Conversation.ConversationActionCallback callback) {
         if (context == null) {
             Log.d(TAG, "launchClearAction: activity is null");
             return;
         }
 
-        if (callContact == null) {
+        if (uri == null) {
             Log.d(TAG, "launchClearAction: conversation is null");
             return;
         }
@@ -67,7 +63,7 @@ public class ActionHelper {
                 .setMessage(R.string.conversation_action_history_clear_message)
                 .setPositiveButton(android.R.string.ok, (dialog, whichButton) -> {
                     if (callback != null) {
-                        callback.clearConversation(callContact);
+                        callback.clearConversation(uri);
                     }
                 })
                 .setNegativeButton(android.R.string.cancel, (dialog, whichButton) -> {
@@ -77,14 +73,14 @@ public class ActionHelper {
     }
 
     public static void launchDeleteAction(final Context context,
-                                                 final CallContact callContact,
+                                                 final Uri uri,
                                                  final Conversation.ConversationActionCallback callback) {
         if (context == null) {
             Log.d(TAG, "launchDeleteAction: activity is null");
             return;
         }
 
-        if (callContact == null) {
+        if (uri == null) {
             Log.d(TAG, "launchDeleteAction: conversation is null");
             return;
         }
@@ -94,7 +90,7 @@ public class ActionHelper {
                 .setMessage(R.string.conversation_action_remove_this_message)
                 .setPositiveButton(android.R.string.ok, (dialog, whichButton) -> {
                     if (callback != null) {
-                        callback.removeConversation(callContact);
+                        callback.removeConversation(uri);
                     }
                 })
                 .setNegativeButton(android.R.string.cancel, (dialog, whichButton) -> {
@@ -104,9 +100,9 @@ public class ActionHelper {
     }
 
     public static void launchCopyNumberToClipboardFromContact(final Context context,
-                                                              final CallContact callContact,
+                                                              final Uri callContact,
                                                               final Conversation.ConversationActionCallback callback) {
-        if (callContact == null) {
+        /*if (callContact == null) {
             Log.d(TAG, "launchCopyNumberToClipboardFromContact: callContact is null");
             return;
         }
@@ -140,7 +136,7 @@ public class ActionHelper {
                     .getDimension(R.dimen.alert_dialog_side_padding_list_view);
             alertDialog.getListView().setPadding(listViewSidePadding, 0, listViewSidePadding, 0);
             alertDialog.show();
-        }
+        }*/
     }
 
     public static Intent getAddNumberIntentForContact(CallContact contact) {
@@ -150,8 +146,8 @@ public class ActionHelper {
         ArrayList<ContentValues> data = new ArrayList<>();
         ContentValues values = new ContentValues();
 
-        Uri number = contact.getPhones().get(0).getNumber();
-        if (number.isRingId()) {
+        Uri number = contact.getUri();
+        if (number.isHexId()) {
             values.put(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Im.CONTENT_ITEM_TYPE);
             values.put(ContactsContract.CommonDataKinds.Im.DATA, number.getRawUriString());
             values.put(ContactsContract.CommonDataKinds.Im.PROTOCOL, ContactsContract.CommonDataKinds.Im.PROTOCOL_CUSTOM);
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 190871c4d34eb8c688bf211b75009edfdcd13034..5eb454eece8b8970046dc881fe59fc601e7ba18c 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,13 +8,11 @@ import android.text.TextUtils;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
-import org.jetbrains.annotations.Contract;
-
-import java.util.Arrays;
 import java.util.List;
 import java.util.Objects;
 
 import cx.ring.fragments.ConversationFragment;
+import cx.ring.model.Conversation;
 import cx.ring.model.Interaction;
 
 public class ConversationPath {
@@ -24,6 +22,10 @@ public class ConversationPath {
         accountId = account;
         conversationId = contact;
     }
+    public ConversationPath(String account, net.jami.model.Uri conversationUri) {
+        accountId = account;
+        conversationId = conversationUri.getUri();
+    }
 
     public ConversationPath(@NonNull Tuple<String, String> path) {
         accountId = path.first;
@@ -57,8 +59,11 @@ public class ConversationPath {
                 .appendEncodedPath(conversationUri.getUri())
                 .build();
     }
+    public static Uri toUri(@NonNull Conversation conversation) {
+        return toUri(conversation.getAccountId(), conversation.getUri());
+    }
     public static Uri toUri(@NonNull Interaction interaction) {
-        return toUri(interaction.getAccount(), new cx.ring.model.Uri(interaction.getConversation().getParticipant()));
+        return toUri(interaction.getAccount(), cx.ring.model.Uri.fromString(interaction.getConversation().getParticipant()));
     }
 
     public Bundle toBundle() {
@@ -108,7 +113,6 @@ public class ConversationPath {
         return null;
     }
 
-    @Contract("null -> null")
     public static ConversationPath fromBundle(@Nullable Bundle bundle) {
         if (bundle != null) {
             String accountId = bundle.getString(ConversationFragment.KEY_ACCOUNT_ID);
@@ -120,7 +124,6 @@ public class ConversationPath {
         return null;
     }
 
-    @Contract("null -> null")
     public static ConversationPath fromIntent(@Nullable Intent intent) {
         if (intent != null) {
             Uri uri = intent.getData();
@@ -132,11 +135,19 @@ public class ConversationPath {
         return null;
     }
 
+    @Override
+    public @NonNull String toString() {
+        return "ConversationPath{" +
+                "accountId='" + accountId + '\'' +
+                ", conversationId='" + conversationId + '\'' +
+                '}';
+    }
+
     @Override
     public boolean equals(@Nullable Object obj) {
         if (obj == this)
             return true;
-        if (obj == null || obj.getClass() != getClass())
+        if (!(obj instanceof ConversationPath))
             return false;
         ConversationPath o = (ConversationPath) obj;
         return Objects.equals(o.accountId, accountId)
@@ -149,6 +160,6 @@ public class ConversationPath {
     }
 
     public cx.ring.model.Uri getConversationUri() {
-        return new cx.ring.model.Uri(conversationId);
+        return cx.ring.model.Uri.fromString(conversationId);
     }
 }
diff --git a/ring-android/app/src/main/java/cx/ring/viewholders/SmartListViewHolder.java b/ring-android/app/src/main/java/cx/ring/viewholders/SmartListViewHolder.java
index 5548921610a63f7aa061fa5aade04169d388b7a4..875325ca56be58e69e85c04711ed748da9880c32 100644
--- a/ring-android/app/src/main/java/cx/ring/viewholders/SmartListViewHolder.java
+++ b/ring-android/app/src/main/java/cx/ring/viewholders/SmartListViewHolder.java
@@ -22,14 +22,14 @@ package cx.ring.viewholders;
 import cx.ring.R;
 import cx.ring.databinding.ItemSmartlistBinding;
 import cx.ring.databinding.ItemSmartlistHeaderBinding;
-import cx.ring.model.CallContact;
 import cx.ring.model.ContactEvent;
 import cx.ring.model.Interaction;
 import cx.ring.model.SipCall;
 import cx.ring.smartlist.SmartListViewModel;
-import cx.ring.utils.BitmapUtils;
+import cx.ring.utils.Log;
 import cx.ring.utils.ResourceMapper;
 import cx.ring.views.AvatarDrawable;
+import io.reactivex.Observable;
 import io.reactivex.disposables.CompositeDisposable;
 
 import android.content.Context;
@@ -40,35 +40,38 @@ import android.view.View;
 import androidx.annotation.NonNull;
 import androidx.recyclerview.widget.RecyclerView;
 
-import com.jakewharton.rxbinding3.view.RxView;
-
 import java.util.concurrent.TimeUnit;
 
 public class SmartListViewHolder extends RecyclerView.ViewHolder {
-    public ItemSmartlistBinding binding;
-    public ItemSmartlistHeaderBinding headerBinding;
+    public final ItemSmartlistBinding binding;
+    public final ItemSmartlistHeaderBinding headerBinding;
 
-    private CompositeDisposable compositeDisposable = new CompositeDisposable();
+    private final CompositeDisposable compositeDisposable = new CompositeDisposable();
 
     public SmartListViewHolder(@NonNull ItemSmartlistBinding b) {
         super(b.getRoot());
         binding = b;
+        headerBinding = null;
     }
 
     public SmartListViewHolder(@NonNull ItemSmartlistHeaderBinding b) {
         super(b.getRoot());
+        binding = null;
         headerBinding = b;
     }
 
     public void bind(final SmartListListeners clickListener, final SmartListViewModel smartListViewModel) {
+        //Log.w("SmartListViewHolder", "bind " + smartListViewModel.getContact() + " " +smartListViewModel.showPresence());
         compositeDisposable.clear();
 
         if (binding != null) {
-            compositeDisposable.add(RxView.clicks(itemView)
+            compositeDisposable.add(Observable.create(e -> itemView.setOnClickListener(e::onNext))
                     .throttleFirst(1000, TimeUnit.MILLISECONDS)
                     .subscribe(v -> clickListener.onItemClick(smartListViewModel)));
-            compositeDisposable.add(RxView.longClicks(itemView)
-                    .subscribe(u -> clickListener.onItemLongClick(smartListViewModel)));
+            itemView.setOnLongClickListener(v -> {
+                clickListener.onItemLongClick(smartListViewModel);
+                return true;
+            });
 
             binding.convParticipant.setText(smartListViewModel.getContactName());
 
@@ -95,11 +98,10 @@ public class SmartListViewHolder extends RecyclerView.ViewHolder {
                 binding.convLastItem.setTypeface(null, Typeface.NORMAL);
             }
 
-            binding.photo.setImageDrawable(
-                    new AvatarDrawable.Builder()
-                            .withContact(smartListViewModel.getContact())
-                            .withCircleCrop(true)
-                            .build(binding.photo.getContext()));
+            binding.photo.setImageDrawable(new AvatarDrawable.Builder()
+                    .withViewModel(smartListViewModel)
+                    .withCircleCrop(true)
+                    .build(binding.photo.getContext()));
         } else if (headerBinding != null) {
             headerBinding.headerTitle.setText(smartListViewModel.getHeaderTitle() == SmartListViewModel.Title.Conversations
                     ? R.string.navigation_item_conversation : R.string.search_results_public_directory);
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 f88328b3f4439ae2fb79ebc2f676d30fd28b0376..550e0ad3e4f569dab70785b8407adec7a98d236e 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
@@ -30,6 +30,7 @@ import android.graphics.PixelFormat;
 import android.graphics.PorterDuff;
 import android.graphics.Rect;
 import android.graphics.RectF;
+import android.graphics.Shader;
 import android.graphics.Typeface;
 import android.graphics.drawable.Drawable;
 
@@ -40,20 +41,28 @@ import cx.ring.model.Account;
 import cx.ring.model.CallContact;
 import cx.ring.model.Conversation;
 import cx.ring.services.VCardServiceImpl;
+import cx.ring.smartlist.SmartListViewModel;
 import cx.ring.utils.DeviceUtils;
 import cx.ring.utils.HashUtils;
 import io.reactivex.Single;
 
 import android.graphics.drawable.VectorDrawable;
 import android.text.TextUtils;
+import android.util.Log;
 import android.util.TypedValue;
 
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
 import java.util.List;
 
 public class AvatarDrawable extends Drawable {
     private static final int SIZE_AB = 36;
+    private static final float SIZE_BORDER = 2f;
+
     private static final float DEFAULT_TEXT_SIZE_PERCENTAGE = 0.5f;
     private static final int PLACEHOLDER_ICON = R.drawable.baseline_account_crop_24;
+    private static final int PLACEHOLDER_ICON_GROUP = R.drawable.baseline_group_24;
     private static final int CHECKED_ICON = R.drawable.baseline_check_circle_24;
     private static final int PRESENCE_COLOR = R.color.green_A700;
 
@@ -72,25 +81,27 @@ public class AvatarDrawable extends Drawable {
     private final PresenceIndicatorInfo presence = new PresenceIndicatorInfo();
 
     private boolean update = true;
+    private final boolean isGroup;
     private int inSize = -1;
 
     private final int minSize;
-    private Bitmap workspace;
-    private Bitmap bitmap;
+    private final List<Bitmap> workspace;
+    private final List<Bitmap> bitmaps;
     private VectorDrawable placeholder;
     private VectorDrawable checkedIcon;
-    private final RectF backgroundBounds = new RectF();
+    private final List<RectF> backgroundBounds;
+    private final List<Rect> inBounds;
     private String avatarText;
     private float textStartXPoint;
     private float textStartYPoint;
     private int color;
 
-    private final Paint clipPaint = new Paint();
+    private final List<Paint> clipPaint;
     private final Paint textPaint = new Paint();
-    private static final Paint drawPaint = new Paint();
     private final Paint presenceFillPaint;
     private final Paint presenceStrokePaint;
     private final Paint checkedPaint;
+    private static final Paint drawPaint = new Paint();
     static {
         drawPaint.setAntiAlias(true);
         drawPaint.setFilterBitmap(true);
@@ -103,13 +114,14 @@ public class AvatarDrawable extends Drawable {
 
     public static class Builder {
 
-        private Bitmap photo = null;
+        private List<Bitmap> photos = null;
         private String name = null;
         private String id = null;
         private boolean circleCrop = false;
         private boolean isOnline = false;
         private boolean showPresence = true;
         private boolean isChecked = false;
+        private boolean isGroup = false;
 
         public Builder() {}
 
@@ -118,7 +130,11 @@ public class AvatarDrawable extends Drawable {
             return this;
         }
         public Builder withPhoto(Bitmap photo) {
-            this.photo = 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) {
@@ -146,6 +162,8 @@ public class AvatarDrawable extends Drawable {
             return withName(TextUtils.isEmpty(profileName) ? username : profileName);
         }
         public Builder withContact(CallContact contact){
+            if (contact == null)
+                return this;
             return withPhoto((Bitmap)contact.getPhoto())
                     .withId(contact.getPrimaryNumber())
                     .withOnlineState(contact.isOnline())
@@ -153,19 +171,58 @@ public class AvatarDrawable extends Drawable {
         }
 
         public Builder withContacts(List<CallContact> contacts) {
-            if (contacts.size() == 1) {
-                return withContact(contacts.get(0));
+            List<Bitmap> bitmaps = new ArrayList<>(contacts.size());
+            int notTheUser = 0;
+            for (CallContact contact : contacts) {
+                if (contact.isUser())
+                    continue;
+                notTheUser++;
+                Bitmap bitmap = (Bitmap) contact.getPhoto();
+                if (bitmap != null) {
+                    bitmaps.add(bitmap);
+                }
+                if (bitmaps.size() == 4)
+                    break;;
+            }
+            if (bitmaps.isEmpty()) {
+                if (notTheUser == 1) {
+                    for (CallContact contact : contacts) {
+                        if (!contact.isUser())
+                            return withContact(contact);
+                    }
+                } else if (notTheUser == 0) {
+                    // Fallback to the user avatar
+                    for (CallContact contact : contacts)
+                        return withContact(contact);
+                }
+                return this;
             } else {
-                return withName("G");
+                return withPhotos(bitmaps);
             }
         }
         public Builder withConversation(Conversation conversation) {
-            return withContacts(conversation.getContacts());
+            return conversation.isSwarm()
+                    ? withContacts(conversation.getContacts()).setGroup()
+                    : withContact(conversation.getContact());
+        }
+
+        private Builder setGroup() {
+            isGroup = true;
+            return this;
+        }
+
+        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)))
+                    .withPresence(vm.showPresence())
+                    .withCheck(vm.isChecked());
         }
 
         public AvatarDrawable build(Context context) {
             AvatarDrawable avatarDrawable = new AvatarDrawable(
-                    context, photo, name, id, circleCrop);
+                    context, photos, name, id, circleCrop, isGroup);
             avatarDrawable.setOnline(isOnline);
             avatarDrawable.setChecked(isChecked);
             avatarDrawable.showPresence = this.showPresence;
@@ -195,7 +252,9 @@ public class AvatarDrawable extends Drawable {
         String username = contact.getUsername();
         avatarText = convertNameToAvatarText(
                 TextUtils.isEmpty(profileName) ? username : profileName);
-        bitmap = (Bitmap)contact.getPhoto();
+        if (bitmaps != null) {
+            bitmaps.set(0, (Bitmap)contact.getPhoto());
+        }
         isOnline = contact.isOnline();
         update = true;
     }
@@ -204,7 +263,7 @@ public class AvatarDrawable extends Drawable {
         update = true;
     }
     public void setPhoto(Bitmap photo) {
-        bitmap = photo;
+        bitmaps.set(0, photo);
         update = true;
     }
     public void setOnline(boolean online) {
@@ -215,19 +274,50 @@ public class AvatarDrawable extends Drawable {
         isChecked = checked;
     }
 
-    private AvatarDrawable(Context context, Bitmap photo, String name, String id, boolean crop) {
+    private AvatarDrawable(Context context, List<Bitmap> photos, String name, String id, boolean crop, boolean group) {
+        //Log.w("AvatarDrawable", "AvatarDrawable " + (photos == null ? null : photos.size()) + " " + name);
         cropCircle = crop;
+        isGroup = group;
         minSize = (int) TypedValue.applyDimension(
                 TypedValue.COMPLEX_UNIT_DIP, SIZE_AB, context.getResources().getDisplayMetrics());
-        if (photo != null) {
+        float borderSize = TypedValue.applyDimension(
+                TypedValue.COMPLEX_UNIT_DIP, SIZE_BORDER, context.getResources().getDisplayMetrics());
+        if (photos != null && photos.size() > 0) {
             avatarText = null;
-            bitmap = photo;
+            bitmaps = photos;
+            if (photos.size() == 1) {
+                backgroundBounds = Collections.singletonList(new RectF());
+                inBounds = Collections.singletonList(null);
+                clipPaint = cropCircle ? Collections.singletonList(new Paint()) : 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);
+                for (Bitmap ignored : bitmaps) {
+                    backgroundBounds.add(new RectF());
+                    inBounds.add(cropCircle ? null : new Rect());
+                    if (cropCircle) {
+                        Paint p = new Paint();
+                        p.setStrokeWidth(borderSize);
+                        p.setColor(Color.WHITE);
+                        p.setStyle(Paint.Style.FILL);
+                        clipPaint.add(p);
+                        workspace.add(null);
+                    }
+                }
+            }
         } else {
-            bitmap = null;
+            workspace = Arrays.asList((Bitmap)null);
+            bitmaps = null;
+            backgroundBounds = null;
+            inBounds = null;
             avatarText = convertNameToAvatarText(name);
             color = ContextCompat.getColor(context, getAvatarColor(id));
+            clipPaint = cropCircle ? Collections.singletonList(new Paint()) : null;
             if (avatarText == null) {
-                placeholder = (VectorDrawable) context.getDrawable(PLACEHOLDER_ICON);
+                placeholder = (VectorDrawable) context.getDrawable(isGroup ? PLACEHOLDER_ICON_GROUP : PLACEHOLDER_ICON);
             } else {
                 textPaint.setColor(Color.WHITE);
                 textPaint.setTypeface(Typeface.SANS_SERIF);
@@ -254,19 +344,42 @@ public class AvatarDrawable extends Drawable {
         checkedPaint.setStyle(Paint.Style.FILL_AND_STROKE);
         checkedPaint.setAntiAlias(true);
 
-        clipPaint.setAntiAlias(true);
+        if (clipPaint != null)
+            for (Paint p : clipPaint)
+                p.setAntiAlias(true);
         textPaint.setAntiAlias(true);
         textPaint.setColor(Color.WHITE);
         textPaint.setTypeface(Typeface.SANS_SERIF);
     }
 
     public AvatarDrawable(AvatarDrawable other) {
+        //Log.w("AvatarDrawable", "AvatarDrawable copy");
         cropCircle = other.cropCircle;
+        isGroup = other.isGroup;
         minSize = other.minSize;
-        bitmap = other.bitmap;
+        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++) {
+                backgroundBounds.add(new RectF());
+            }
+        }
+
+        inBounds = other.inBounds;
         color = other.color;
         placeholder = other.placeholder;
         avatarText = other.avatarText;
+        workspace = new ArrayList<>(other.workspace.size());
+        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++) {
+                clipPaint.add(new Paint(other.clipPaint.get(i)));
+                clipPaint.get(i).setShader(null);
+            }
+        }
 
         isOnline = other.isOnline;
         isChecked = other.isChecked;
@@ -275,7 +388,6 @@ public class AvatarDrawable extends Drawable {
         presenceStrokePaint = other.presenceStrokePaint;
         checkedPaint = other.checkedPaint;
 
-        clipPaint.setAntiAlias(true);
         textPaint.setAntiAlias(true);
         textPaint.setColor(Color.WHITE);
         textPaint.setTypeface(Typeface.SANS_SERIF);
@@ -283,18 +395,35 @@ public class AvatarDrawable extends Drawable {
 
     @Override
     public void draw(@NonNull Canvas finalCanvas) {
-        if (workspace == null)
+        if (workspace.get(0) == null)
             return;
         if (update) {
-            drawActual(new Canvas(workspace));
+            for (int i = 0, s = workspace.size(); i < s; i++)
+                drawActual(i, new Canvas(workspace.get(i)));
             update = false;
         }
         if (cropCircle) {
-            int d = Math.min(getBounds().width(), getBounds().height());
-            int r = d / 2;
-            finalCanvas.drawCircle(getBounds().centerX(), getBounds().centerY(), r, clipPaint);
+            float r =  Math.min(getBounds().width(), getBounds().height()) / 2;
+            int cx = getBounds().centerX();
+            float cy = getBounds().height() / 2;
+            int i = 0;
+            final float ratio = 1.333333f;
+            for (Paint paint : clipPaint) {
+                finalCanvas.drawCircle(cx, getBounds().bottom - 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);
+                    paint.setShader(s);
+                    paint.setStyle(Paint.Style.FILL);
+                }
+                i++;
+                r /= ratio;
+                cy /= ratio;
+            }
         } else {
-            finalCanvas.drawBitmap(workspace, null, getBounds(), drawPaint);
+            finalCanvas.drawBitmap(workspace.get(0), null, getBounds(), drawPaint);
         }
         if (showPresence && isOnline) {
             drawPresence(finalCanvas);
@@ -304,9 +433,16 @@ public class AvatarDrawable extends Drawable {
         }
     }
 
-    private void drawActual(@NonNull Canvas canvas) {
-        if (bitmap != null) {
-            canvas.drawBitmap(bitmap, null, backgroundBounds, drawPaint);
+    private void drawActual(int i, @NonNull Canvas canvas) {
+        if (bitmaps != null) {
+            if (cropCircle) {
+                canvas.drawBitmap(bitmaps.get(i), inBounds.get(i), backgroundBounds.get(i), drawPaint);
+            } else {
+                if (backgroundBounds.size() == bitmaps.size())
+                    for (int n = 0, s = bitmaps.size(); n < s; n++) {
+                        canvas.drawBitmap(bitmaps.get(n), inBounds.get(n), backgroundBounds.get(n), drawPaint);
+                    }
+            }
         } else {
             canvas.drawColor(color);
             if (avatarText != null) {
@@ -342,6 +478,54 @@ public class AvatarDrawable extends Drawable {
         }
     }
 
+    private static Rect getSubBounds(@NonNull Rect bounds, int total, int i)  {
+        if (total == 1)
+            return bounds;
+
+        if (total == 2 || (total == 3 && i == 0)) {
+            //Rect zone = getSubZone(bounds, 2, 1);
+            int w = bounds.width() / 2;
+            return (i == 0)
+                    ? new Rect(bounds.left, bounds.top, bounds.left + w, bounds.bottom)
+                    : new Rect(bounds.left + w, bounds.top, bounds.right, bounds.bottom);
+        }
+        if (total == 3 || (total == 4 && (i == 1 || i == 2))) {
+            int w = bounds.width() / 2;
+            int h = bounds.height() / 2;
+            return (i == 1)
+                    ? new Rect(bounds.left + w, bounds.top, bounds.right, bounds.top + h)
+                    : new Rect(bounds.left + w, bounds.top + h, bounds.right, bounds.bottom);
+        }
+        if (total == 4) {
+            int w = bounds.width() / 2;
+            int h = bounds.height() / 2;
+            return (i == 0)
+                    ? new Rect(bounds.left, bounds.top, bounds.left + w, bounds.top + h)
+                    : new Rect(bounds.left, bounds.top + h, bounds.left + w, bounds.bottom);
+        }
+        return null;
+    }
+
+    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;
+        int h;
+        if (outfit == (a < b)) {
+            w = iw;
+            h = (iw * bh) / bw;
+        } else {
+            w = (ih * bw) / bh;
+            h = ih;
+        }
+        int x = (iw - w) / 2;
+        int y = (ih - h) / 2;
+        if (ret instanceof Rect)
+            ((Rect)ret).set(x, y, x + w, y + h);
+        else if (ret instanceof RectF)
+            ((RectF)ret).set(x, y, x + w, y + h);
+    }
+
     @Override
     protected void onBoundsChange(Rect bounds) {
         //if (showPresence)
@@ -352,41 +536,48 @@ public class AvatarDrawable extends Drawable {
             int cy = (bounds.height()-d)/2;
             placeholder.setBounds(cx, cy, cx + d, cy + d);
         }
-        if (bitmap != null) {
-            int iw = cropCircle ? d : bounds.width();
-            int ih = cropCircle ? d : bounds.height();
-            int a = bitmap.getWidth() * ih;
-            int b = bitmap.getHeight() * iw;
-            int w;
-            int h;
-            if (a < b) {
-                w = iw;
-                h  = (iw * bitmap.getHeight())/bitmap.getWidth();
-            } else {
-                w  = (ih * bitmap.getWidth())/bitmap.getHeight();
-                h = ih;
+        int iw = cropCircle ? d : bounds.width();
+        int ih = cropCircle ? d : bounds.height();
+        for (int i=0, n=workspace.size(); i<n; i++) {
+            if (workspace.get(i) != null) {
+                workspace.get(i).recycle();
+                workspace.set(i, null);
             }
-            int cx = (iw - w)/2;
-            int cy = (ih - h)/2;
-            backgroundBounds.set(cx, cy, cx + w, h + cy);
-        } else {
-            setAvatarTextValues(bounds);
+        }
+        if (iw <= 0 || ih <= 0) {
+            for (Paint p : clipPaint)
+                p.setShader(null);
+            return;
         }
         if (cropCircle) {
-            if (d > 0) {
-                workspace = Bitmap.createBitmap(d, d, Bitmap.Config.ARGB_8888);
-                clipPaint.setShader(new BitmapShader(workspace, BitmapShader.TileMode.CLAMP,
-                                    BitmapShader.TileMode.CLAMP));
+            for (int i = 0, s = workspace.size(); i < s; i++) {
+                Bitmap workspacei = Bitmap.createBitmap(iw, ih, Bitmap.Config.ARGB_8888);
+                workspace.set(i, workspacei);
+                clipPaint.get(i).setShader(new BitmapShader(workspacei, BitmapShader.TileMode.CLAMP, BitmapShader.TileMode.CLAMP));
+            }
+        } else {
+            workspace.set(0, Bitmap.createBitmap(bounds.width(), bounds.height(), Bitmap.Config.ARGB_8888));
+        }
+
+        if (bitmaps != null) {
+            if (bitmaps.size() == 1 || cropCircle) {
+                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 {
-                clipPaint.setShader(null);
-                if (workspace != null) {
-                    workspace.recycle();
-                    workspace = null;
+                Rect realBounds = cropCircle ? new Rect(0, 0, iw, ih) : bounds;
+                for (int i=0; i<bitmaps.size(); i++) {
+                    Bitmap bitmap = bitmaps.get(i);
+                    Rect subBounds = getSubBounds(realBounds, bitmaps.size(), i);
+                    if (subBounds != null) {
+                        fit(bitmap.getWidth(), bitmap.getHeight(), subBounds.width(), subBounds.height(), false, inBounds.get(i));
+                        backgroundBounds.get(i).set(subBounds);
+                    }
                 }
             }
         } else {
-            workspace = Bitmap.createBitmap(bounds.width(), bounds.height(),
-                                            Bitmap.Config.ARGB_8888);
+            setAvatarTextValues(bounds);
         }
         update = true;
     }
diff --git a/ring-android/app/src/main/java/cx/ring/views/ConversationViewHolder.java b/ring-android/app/src/main/java/cx/ring/views/ConversationViewHolder.java
index 99e4c6c189f3e48bfdaff94db81795baec737e58..0795e32863caa5c659e4b51b7347cd856f6e14df 100644
--- a/ring-android/app/src/main/java/cx/ring/views/ConversationViewHolder.java
+++ b/ring-android/app/src/main/java/cx/ring/views/ConversationViewHolder.java
@@ -58,7 +58,6 @@ public class ConversationViewHolder extends RecyclerView.ViewHolder {
     public MediaPlayer player;
     public TextureView video;
     public Surface surface = null;
-    public String mCid;
     public UiUpdater updater;
     public LinearLayout mCallInfoLayout, mFileInfoLayout, mAudioInfoLayout;
     public ValueAnimator animator;
@@ -76,7 +75,7 @@ public class ConversationViewHolder extends RecyclerView.ViewHolder {
             mHistDetailTxt = v.findViewById(R.id.call_details_txt);
             mIcon = v.findViewById(R.id.call_icon);
             mCallInfoLayout = v.findViewById(R.id.callInfoLayout);
-        } else {
+        } else if (type != ConversationAdapter.MessageType.INVALID) {
             switch (type) {
                 // common layout elements
                 case INCOMING_TEXT_MESSAGE:
diff --git a/ring-android/app/src/main/java/cx/ring/views/SwitchButton.java b/ring-android/app/src/main/java/cx/ring/views/SwitchButton.java
index cb585d3a61b53b6cf7246d1ff18adcbe223389c3..0d62b8d410bbf26ea308c2d5f3964e07fb5ab0d2 100644
--- a/ring-android/app/src/main/java/cx/ring/views/SwitchButton.java
+++ b/ring-android/app/src/main/java/cx/ring/views/SwitchButton.java
@@ -120,7 +120,6 @@ public class SwitchButton extends CompoundButton {
         setClickable(true);
 
         mStatus = status;
-
         mBackColor = backColor;
 
         float margin = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, DEFAULT_THUMB_MARGIN_DP, getResources().getDisplayMetrics());
diff --git a/ring-android/app/src/main/res/layout-w720dp-land/activity_home.xml b/ring-android/app/src/main/res/layout-w720dp-land/activity_home.xml
index a996a6fc4c09f49c9a8835bd63fe81a60cfcf43e..7d7194710309a567ac4ba86a52fa8e4fcb5d5cbd 100644
--- a/ring-android/app/src/main/res/layout-w720dp-land/activity_home.xml
+++ b/ring-android/app/src/main/res/layout-w720dp-land/activity_home.xml
@@ -1,8 +1,8 @@
 <androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
-    xmlns:tools="http://schemas.android.com/tools"
     android:orientation="vertical">
 
     <com.google.android.material.appbar.AppBarLayout
@@ -17,9 +17,9 @@
             android:layout_width="match_parent"
             android:layout_height="?attr/actionBarSize"
             android:layout_alignParentTop="true"
-            app:contentInsetStart="20dp"
             android:clipChildren="false"
-            android:clipToPadding="false">
+            android:clipToPadding="false"
+            app:contentInsetStart="20dp">
 
             <cx.ring.views.SwitchButton
                 android:id="@+id/account_switch"
@@ -60,7 +60,8 @@
                         android:layout_width="36dp"
                         android:layout_height="36dp"
                         android:layout_alignParentStart="true"
-                        android:layout_centerVertical="true" />
+                        android:layout_centerVertical="true"
+                        tools:src="@drawable/baseline_person_24" />
 
                     <TextView
                         android:id="@+id/contact_title"
@@ -69,8 +70,11 @@
                         android:layout_alignTop="@+id/contact_image"
                         android:layout_marginStart="16dp"
                         android:layout_toEndOf="@+id/contact_image"
+                        android:ellipsize="middle"
+                        android:singleLine="true"
                         android:textColor="@color/textColorPrimary"
-                        android:textSize="16sp" />
+                        android:textSize="16sp"
+                        tools:text="@tools:sample/first_names" />
 
                     <TextView
                         android:id="@+id/contact_subtitle"
@@ -79,9 +83,10 @@
                         android:layout_below="@+id/contact_title"
                         android:layout_alignStart="@id/contact_title"
                         android:layout_toEndOf="@+id/contact_image"
-                        android:ellipsize="end"
-                        android:maxLines="1"
-                        android:textSize="14sp" />
+                        android:ellipsize="middle"
+                        android:singleLine="true"
+                        android:textSize="14sp"
+                        tools:text="@tools:sample/full_names" />
 
                 </RelativeLayout>
 
@@ -95,16 +100,14 @@
         android:id="@+id/main_frame"
         android:layout_width="match_parent"
         android:layout_height="match_parent"
-        android:layout_below="@id/main_toolbar"
-        />
+        android:layout_below="@id/main_toolbar" />
 
     <com.google.android.material.bottomnavigation.BottomNavigationView
         android:id="@+id/navigation_view"
         android:layout_width="320dp"
         android:layout_height="@dimen/navigation_bottom_height"
-        app:menu="@menu/navigation_bar"
-        app:labelVisibilityMode="labeled"
         android:layout_gravity="bottom"
-        />
+        app:labelVisibilityMode="labeled"
+        app:menu="@menu/navigation_bar" />
 
 </androidx.coordinatorlayout.widget.CoordinatorLayout>
\ No newline at end of file
diff --git a/ring-android/app/src/main/res/layout-w720dp-land/frag_smartlist.xml b/ring-android/app/src/main/res/layout-w720dp-land/frag_smartlist.xml
index 2c7fbce48505aca5954946a0367b54a4caf72647..684907ed753360c29d7a4258a370b8bee286c908 100644
--- a/ring-android/app/src/main/res/layout-w720dp-land/frag_smartlist.xml
+++ b/ring-android/app/src/main/res/layout-w720dp-land/frag_smartlist.xml
@@ -9,10 +9,9 @@
 
     <com.google.android.material.card.MaterialCardView
         android:id="@+id/qr_code"
-        android:layout_width="300dp"
+        android:layout_width="128dp"
         android:layout_height="wrap_content"
         style="@style/ButtonOutLined"
-        android:layout_toLeftOf="@+id/separator"
         android:layout_alignParentLeft="true"
         android:layout_marginLeft="20dp"
         android:layout_marginRight="20dp"
@@ -33,6 +32,31 @@
 
     </com.google.android.material.card.MaterialCardView>
 
+    <com.google.android.material.card.MaterialCardView
+        android:id="@+id/new_group"
+        style="@style/Widget.MaterialComponents.Button.OutlinedButton"
+        android:layout_toLeftOf="@+id/separator"
+        android:layout_marginTop="?actionBarSize"
+        android:layout_width="128dp"
+        android:layout_height="wrap_content"
+        android:layout_toEndOf="@id/qr_code"
+        android:visibility="gone"
+        app:cardCornerRadius="@dimen/button_corner_radius"
+        tools:visibility="visible">
+
+        <TextView
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_gravity="center"
+            android:drawableLeft="@drawable/baseline_group_add_24"
+            android:drawablePadding="8dp"
+            android:drawableTint="@color/colorPrimary"
+            android:text="Add group"
+            android:textColor="@color/colorPrimary"
+            android:textSize="16sp" />
+
+    </com.google.android.material.card.MaterialCardView>
+
     <androidx.coordinatorlayout.widget.CoordinatorLayout
         android:id="@+id/list_coordinator"
         android:layout_width="320dp"
@@ -57,7 +81,7 @@
                 <ImageView
                     android:layout_width="128dp"
                     android:layout_height="128dp"
-                    android:tint="@color/darker_gray"
+                    app:tint="@color/darker_gray"
                     android:src="@drawable/baseline_forum_24" />
 
                 <TextView
diff --git a/ring-android/app/src/main/res/layout/frag_acc_summary.xml b/ring-android/app/src/main/res/layout/frag_acc_summary.xml
index 214056396460deaac4989058dd1fd01c45a17d66..4d80b1e6600799380e0d34e24c953bc09761bc01 100644
--- a/ring-android/app/src/main/res/layout/frag_acc_summary.xml
+++ b/ring-android/app/src/main/res/layout/frag_acc_summary.xml
@@ -73,16 +73,18 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
                     app:endIconDrawable="@drawable/baseline_edit_twoton_24dp"
                     app:endIconMode="custom"
                     app:endIconTint="@color/colorPrimary"
+                    app:hintEnabled="true"
                     app:hintTextColor="@color/text_hint">
 
                     <com.google.android.material.textfield.TextInputEditText
                         android:id="@+id/username"
                         android:layout_width="match_parent"
                         android:layout_height="wrap_content"
-                        android:textColor="@color/text_color"
-                        android:textStyle="bold"
+                        android:inputType="textPersonName"
+                        android:maxLines="1"
                         android:singleLine="true"
-                        android:maxLines="1"/>
+                        android:textColor="@color/text_color"
+                        android:textStyle="bold" />
 
                 </com.google.android.material.textfield.TextInputLayout>
 
@@ -139,12 +141,27 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
                 android:layout_width="match_parent"
                 android:layout_height="wrap_content">
 
-                <cx.ring.views.TwoButtonEditText
-                    android:id="@+id/registered_name"
+                <com.google.android.material.textfield.TextInputLayout
                     android:layout_width="match_parent"
                     android:layout_height="wrap_content"
-                    android:enabled="false"
-                    android:hint="@string/registered_username" />
+                    android:hint="@string/registered_username"
+                    android:textColorHint="@color/text_hint"
+                    app:hintEnabled="true"
+                    app:hintTextColor="@color/text_hint"
+                    android:enabled="false">
+
+                    <com.google.android.material.textfield.TextInputEditText
+                        android:id="@+id/registered_name"
+                        android:layout_width="match_parent"
+                        android:layout_height="wrap_content"
+                        android:inputType="textPersonName"
+                        android:maxLines="1"
+                        android:singleLine="true"
+                        android:textColor="@color/text_color"
+                        android:textStyle="bold"
+                        android:textIsSelectable="true"/>
+
+                </com.google.android.material.textfield.TextInputLayout>
 
                 <com.google.android.material.chip.Chip
                     android:id="@+id/register_name"
@@ -182,13 +199,29 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
             </LinearLayout>
 
-            <cx.ring.views.TwoButtonEditText
-                android:id="@+id/identity"
+            <com.google.android.material.textfield.TextInputLayout
                 android:layout_width="match_parent"
                 android:layout_height="wrap_content"
-                android:layout_marginTop="12dp"
-                android:enabled="false"
-                android:hint="@string/ring_account_identity" />
+                android:hint="@string/ring_account_identity"
+                android:textColorHint="@color/text_hint"
+                app:hintEnabled="true"
+                app:hintTextColor="@color/text_hint"
+                android:layout_marginTop="12dp">
+
+                <com.google.android.material.textfield.TextInputEditText
+                    android:id="@+id/identity"
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:inputType="none"
+                    android:editable="false"
+                    android:singleLine="true"
+                    android:textColor="@color/text_color"
+                    android:textStyle="bold"
+                    android:textIsSelectable="true"
+                    android:ellipsize="end"
+                    />
+
+            </com.google.android.material.textfield.TextInputLayout>
 
             <cx.ring.views.TwoButtonEditText
                 android:id="@+id/linked_devices"
diff --git a/ring-android/app/src/main/res/layout/frag_contact_picker.xml b/ring-android/app/src/main/res/layout/frag_contact_picker.xml
new file mode 100644
index 0000000000000000000000000000000000000000..09b65f0ea97a88564b2a8bef05610c89b7974726
--- /dev/null
+++ b/ring-android/app/src/main/res/layout/frag_contact_picker.xml
@@ -0,0 +1,69 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.constraintlayout.widget.ConstraintLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    tools:context=".fragments.ContactPickerFragment"
+    android:animateLayoutChanges="true">
+
+    <com.google.android.material.appbar.AppBarLayout
+        style="@style/Widget.MaterialComponents.AppBarLayout.Surface"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:id="@+id/selected_contacts_tooolbar"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="parent"
+        android:animateLayoutChanges="true">
+
+        <com.google.android.material.appbar.MaterialToolbar
+            style="@style/Widget.MaterialComponents.Toolbar.Surface"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            app:title="Create a swarm"
+            app:navigationIcon="@drawable/baseline_group_add_24" />
+
+        <com.google.android.material.chip.ChipGroup
+            style="@style/Widget.MaterialComponents.ChipGroup"
+            android:id="@+id/selected_contacts"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:minHeight="100dp"
+            android:animateLayoutChanges="true"
+            android:paddingHorizontal="8dp">
+
+        </com.google.android.material.chip.ChipGroup>
+
+    </com.google.android.material.appbar.AppBarLayout>
+
+    <androidx.recyclerview.widget.RecyclerView
+        android:id="@+id/contact_list"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:minHeight="200dp"
+        android:paddingTop="16dp"
+        android:clipToPadding="false"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toBottomOf="@id/selected_contacts_tooolbar"
+        tools:listitem="@layout/item_contact"
+        app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"/>
+
+    <com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
+        android:id="@+id/create_group_btn"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_margin="24dp"
+        android:layout_marginStart="381dp"
+        android:layout_marginEnd="16dp"
+        android:text="Create swarm"
+        app:icon="@drawable/baseline_group_add_24"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        android:enabled="false"
+        android:animateLayoutChanges="true" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
diff --git a/ring-android/app/src/main/res/layout/frag_smartlist.xml b/ring-android/app/src/main/res/layout/frag_smartlist.xml
index b3719022e0e02466f5fdb313b7b5bfa691f5aa2a..146aebe82339e28ef11ab6964e62a40c7636ee3b 100644
--- a/ring-android/app/src/main/res/layout/frag_smartlist.xml
+++ b/ring-android/app/src/main/res/layout/frag_smartlist.xml
@@ -28,11 +28,10 @@ along with this program; if not, write to the Free Software
 
     <com.google.android.material.card.MaterialCardView
         android:id="@+id/qr_code"
-        android:layout_width="@dimen/wizard_button_width"
+        android:layout_width="@dimen/wizard_button_width_half"
         android:layout_height="wrap_content"
         style="@style/ButtonOutLined"
-        android:layout_centerHorizontal="true"
-        android:layout_marginTop="8dp"
+        android:layout_margin="8dp"
         app:cardCornerRadius="@dimen/button_corner_radius"
         android:visibility="gone"
         tools:visibility="visible" >
@@ -49,6 +48,30 @@ along with this program; if not, write to the Free Software
 
     </com.google.android.material.card.MaterialCardView>
 
+    <com.google.android.material.card.MaterialCardView
+        android:id="@+id/new_group"
+        style="@style/Widget.MaterialComponents.Button.OutlinedButton"
+        android:layout_width="@dimen/wizard_button_width_half"
+        android:layout_height="wrap_content"
+        android:layout_toEndOf="@id/qr_code"
+        app:cardCornerRadius="@dimen/button_corner_radius"
+        android:layout_margin="8dp"
+        android:visibility="gone"
+        tools:visibility="visible">
+
+        <TextView
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_gravity="center"
+            android:drawableLeft="@drawable/baseline_group_add_24"
+            android:drawablePadding="8dp"
+            android:drawableTint="@color/colorPrimary"
+            android:text="Add group"
+            android:textColor="@color/colorPrimary"
+            android:textSize="16sp" />
+
+    </com.google.android.material.card.MaterialCardView>
+
     <androidx.coordinatorlayout.widget.CoordinatorLayout
         android:id="@+id/list_coordinator"
         android:layout_below="@id/qr_code"
@@ -85,12 +108,14 @@ along with this program; if not, write to the Free Software
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
             android:layout_gravity="center"
-            android:indeterminate="true" />
+            android:indeterminate="true"
+            tools:visibility="gone"/>
 
         <androidx.recyclerview.widget.RecyclerView
             android:id="@+id/confs_list"
             android:layout_width="match_parent"
             android:layout_height="match_parent"
+            app:layout_constraintTop_toBottomOf="@id/qr_code"
             android:layout_marginStart="0dp"
             android:clipToPadding="false"
             android:divider="@null"
@@ -98,7 +123,7 @@ along with this program; if not, write to the Free Software
             android:paddingTop="8dp"
             android:paddingBottom="8dp"
             tools:listitem="@layout/item_smartlist"
-            tools:visibility="gone"/>
+            tools:visibility="visible"/>
 
         <com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
             style="@style/Widget.AppTheme.MainActionButton"
diff --git a/ring-android/app/src/main/res/layout/item_two_button_edittext.xml b/ring-android/app/src/main/res/layout/item_two_button_edittext.xml
index 98c3029afe6a6dca0effd7e2f013757703fc2c6d..263f29f7253bbb068a723b0a7adc9ae9c27a6235 100644
--- a/ring-android/app/src/main/res/layout/item_two_button_edittext.xml
+++ b/ring-android/app/src/main/res/layout/item_two_button_edittext.xml
@@ -1,6 +1,7 @@
 <?xml version="1.0" encoding="utf-8"?>
 <merge xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
     android:layout_width="match_parent"
     android:layout_height="wrap_content">
 
@@ -10,7 +11,8 @@
         android:layout_height="wrap_content"
         android:layout_weight="1"
         android:textColorHint="@color/text_hint"
-        app:hintTextColor="@color/text_hint">
+        app:hintTextColor="@color/text_hint"
+        tools:hint="Name">
 
         <com.google.android.material.textfield.TextInputEditText
             android:id="@+id/edit_text"
@@ -22,7 +24,8 @@
             android:singleLine="true"
             android:textColor="@color/text_color"
             android:textIsSelectable="true"
-            android:textStyle="bold" />
+            android:textStyle="bold"
+            tools:text="@tools:sample/full_names"/>
 
     </com.google.android.material.textfield.TextInputLayout>
 
@@ -34,7 +37,7 @@
         android:layout_marginEnd="2dp"
         android:background="?selectableItemBackgroundBorderless"
         android:padding="2dp"
-        android:visibility="gone" />
+        tools:src="@drawable/baseline_add_24"/>
 
     <androidx.appcompat.widget.AppCompatImageButton
         android:id="@+id/btn_right"
diff --git a/ring-android/app/src/main/res/values/dimens.xml b/ring-android/app/src/main/res/values/dimens.xml
index 3067f2376fde189dafd2494684485e753c2f3464..36a7caad61b7362a0fe4a49179289ea88a70bc54 100644
--- a/ring-android/app/src/main/res/values/dimens.xml
+++ b/ring-android/app/src/main/res/values/dimens.xml
@@ -84,6 +84,8 @@ along with this program; if not, write to the Free Software
 
     <!--  Wizard  -->
     <dimen name="wizard_button_width">340dp</dimen>
+    <dimen name="wizard_button_width_half">200dp</dimen>
+
     <dimen name="wizard_button_corner_radius">@dimen/button_corner_radius</dimen>
     <dimen name="wizard_button_text_size">12sp</dimen>
     <dimen name="wizard_button_padding">@dimen/button_padding</dimen>
diff --git a/ring-android/app/src/main/res/values/strings.xml b/ring-android/app/src/main/res/values/strings.xml
index 5ed4a68eb165462891b891a1bc9b10c9d04e3e76..66a1fe34d28fe0560e6bef6fb68d83a4b3145f3f 100644
--- a/ring-android/app/src/main/res/values/strings.xml
+++ b/ring-android/app/src/main/res/values/strings.xml
@@ -239,6 +239,12 @@ along with this program; if not, write to the Free Software
     <string name="block_contact_completed">%1$s was blocked.</string>
     <string name="conversation_contact_is_typing">Contact is typing…</string>
 
+    <string name="conversation_preference_color">Change conversation color</string>
+    <string name="conversation_preference_emoji">Change conversation emoji</string>
+    <string name="conversation_type_contact">Jami contact</string>
+    <string name="conversation_type_private">Private swarm</string>
+    <string name="conversation_type_group">Group swarm</string>
+
     <!-- Contacts -->
     <string name="add_call_contact_number_to_contacts">Add %1$s?</string>
     <string name="prompt_new_password">New password</string>
diff --git a/ring-android/libringclient/src/main/java/cx/ring/call/CallPresenter.java b/ring-android/libringclient/src/main/java/cx/ring/call/CallPresenter.java
index c687b98f6ce25247fe22ce2ef039e6a323ee3b79..ae402b826b7d8905c2006c7ddd611d3b782f4585 100644
--- a/ring-android/libringclient/src/main/java/cx/ring/call/CallPresenter.java
+++ b/ring-android/libringclient/src/main/java/cx/ring/call/CallPresenter.java
@@ -46,6 +46,7 @@ import io.reactivex.Maybe;
 import io.reactivex.Observable;
 import io.reactivex.Observer;
 import io.reactivex.Scheduler;
+import io.reactivex.annotations.NonNull;
 import io.reactivex.disposables.Disposable;
 import io.reactivex.subjects.BehaviorSubject;
 import io.reactivex.subjects.Subject;
@@ -150,8 +151,8 @@ public class CallPresenter extends RootPresenter<CallView> {
                 }));*/
     }
 
-    public void initOutGoing(String accountId, String contactRingId, boolean audioOnly) {
-        if (accountId == null || contactRingId == null) {
+    public void initOutGoing(String accountId, Uri conversationUri, String contactId, boolean audioOnly) {
+        if (accountId == null || contactId == null) {
             Log.e(TAG, "initOutGoing: null account or contact");
             hangupCall();
             return;
@@ -162,7 +163,7 @@ public class CallPresenter extends RootPresenter<CallView> {
         //getView().blockScreenRotation();
 
         Observable<Conference> callObservable = mCallService
-                .placeCall(accountId, StringUtils.toNumber(contactRingId), audioOnly)
+                .placeCall(accountId, conversationUri, Uri.fromString(StringUtils.toNumber(contactId)), audioOnly)
                 //.map(mCallService::getConference)
                 .flatMapObservable(call -> mCallService.getConfUpdates(call))
                 .share();
@@ -635,18 +636,18 @@ public class CallPresenter extends RootPresenter<CallView> {
         mCallService.playDtmf(s.toString());
     }
 
-    public void addConferenceParticipant(String accountId, String contactId) {
-        mCompositeDisposable.add(mConversationFacade.startConversation(accountId, new Uri(contactId))
+    public void addConferenceParticipant(String accountId, Uri contactUri) {
+        mCompositeDisposable.add(mConversationFacade.startConversation(accountId, contactUri)
                 .map(Conversation::getCurrentCalls)
                 .subscribe(confs -> {
                     if (confs.isEmpty()) {
                         final Observer<SipCall> pendingObserver = new Observer<SipCall>() {
                             private SipCall call = null;
                             @Override
-                            public void onSubscribe(Disposable d) {}
+                            public void onSubscribe(@NonNull Disposable d) {}
 
                             @Override
-                            public void onNext(SipCall sipCall) {
+                            public void onNext(@NonNull SipCall sipCall) {
                                 if (call == null) {
                                     call = sipCall;
                                     mPendingCalls.add(sipCall);
@@ -668,7 +669,7 @@ public class CallPresenter extends RootPresenter<CallView> {
                         };
 
                         // Place new call, join to conference when answered
-                        Maybe<SipCall> newCall = mCallService.placeCallObservable(accountId, contactId, mAudioOnly)
+                        Maybe<SipCall> newCall = mCallService.placeCallObservable(accountId, null, contactUri, mAudioOnly)
                                 .doOnEach(pendingObserver)
                                 .filter(SipCall::isOnGoing)
                                 .firstElement()
diff --git a/ring-android/libringclient/src/main/java/cx/ring/contactrequests/BlackListPresenter.java b/ring-android/libringclient/src/main/java/cx/ring/contactrequests/BlackListPresenter.java
index 7c18f419baf59fe4c96aacdda20b5d217252ca98..8a92725cd445a2f63faab01593e0cffe28b6e098 100644
--- a/ring-android/libringclient/src/main/java/cx/ring/contactrequests/BlackListPresenter.java
+++ b/ring-android/libringclient/src/main/java/cx/ring/contactrequests/BlackListPresenter.java
@@ -87,7 +87,7 @@ public class BlackListPresenter extends RootPresenter<BlackListView> {
     }
 
     public void unblockClicked(CallContact contact) {
-        String contactId = contact.getPhones().get(0).getNumber().getRawRingId();
+        String contactId = contact.getUri().getRawRingId();
         mAccountService.addContact(mAccountID, contactId);
     }
 }
diff --git a/ring-android/libringclient/src/main/java/cx/ring/contactrequests/ContactRequestsPresenter.java b/ring-android/libringclient/src/main/java/cx/ring/contactrequests/ContactRequestsPresenter.java
index d777074778d311074492afeccc042c448e3e183a..c5678e6a31f8b151f9ef790c1ba059115d01ba45 100644
--- a/ring-android/libringclient/src/main/java/cx/ring/contactrequests/ContactRequestsPresenter.java
+++ b/ring-android/libringclient/src/main/java/cx/ring/contactrequests/ContactRequestsPresenter.java
@@ -26,16 +26,14 @@ import javax.inject.Inject;
 import javax.inject.Named;
 
 import cx.ring.facades.ConversationFacade;
-import cx.ring.model.CallContact;
-import cx.ring.model.Conversation;
+import cx.ring.model.Account;
+import cx.ring.model.Uri;
 import cx.ring.mvp.RootPresenter;
 import cx.ring.services.AccountService;
-import cx.ring.services.ContactService;
 import cx.ring.smartlist.SmartListViewModel;
 import cx.ring.utils.Log;
 import io.reactivex.Observable;
 import io.reactivex.Scheduler;
-import io.reactivex.disposables.CompositeDisposable;
 import io.reactivex.subjects.BehaviorSubject;
 
 public class ContactRequestsPresenter extends RootPresenter<ContactRequestsView> {
@@ -45,7 +43,7 @@ public class ContactRequestsPresenter extends RootPresenter<ContactRequestsView>
     private final Scheduler mUiScheduler;
     private final AccountService mAccountService;
     private final ConversationFacade mConversationFacade;
-    private final BehaviorSubject<String> mAccount = BehaviorSubject.create();
+    private final BehaviorSubject<Account> mAccount = BehaviorSubject.create();
 
     @Inject
     ContactRequestsPresenter(ConversationFacade conversationFacade, AccountService accountService, @Named("UiScheduler") Scheduler scheduler) {
@@ -57,7 +55,7 @@ public class ContactRequestsPresenter extends RootPresenter<ContactRequestsView>
     @Override
     public void bindView(ContactRequestsView view) {
         super.bindView(view);
-        mCompositeDisposable.add(mConversationFacade.getPendingList(mAccount.map(mAccountService::getAccount))
+        mCompositeDisposable.add(mConversationFacade.getPendingList(mAccount)
                 .switchMap(viewModels -> viewModels.isEmpty() ? SmartListViewModel.EMPTY_RESULTS
                         : Observable.combineLatest(viewModels, obs -> {
                     List<SmartListViewModel> vms = new ArrayList<>(obs.length);
@@ -66,16 +64,25 @@ public class ContactRequestsPresenter extends RootPresenter<ContactRequestsView>
                     return vms;
                 }))
                 .observeOn(mUiScheduler)
-                .subscribe(viewModels -> {
-                    getView().updateView(viewModels);
-                }, e -> Log.d(TAG, "updateList subscribe onError", e)));
+                .subscribe(viewModels -> getView().updateView(viewModels),
+                        e -> Log.d(TAG, "updateList subscribe onError", e)));
+    }
+
+    @Override
+    public void unbindView() {
+        mAccount.onComplete();
+        super.unbindView();
     }
 
     public void updateAccount(String accountId) {
-        mAccount.onNext(accountId);
+        if (accountId == null) {
+            mAccountService.getCurrentAccountSubject().subscribe(mAccount);
+        } else {
+            mAccount.onNext(mAccountService.getAccount(accountId));
+        }
     }
 
-    public void contactRequestClicked(String accountId, CallContact contactId) {
-        getView().goToConversation(accountId, contactId.getPrimaryNumber());
+    public void contactRequestClicked(String accountId, Uri uri) {
+        getView().goToConversation(accountId, uri);
     }
 }
diff --git a/ring-android/libringclient/src/main/java/cx/ring/contactrequests/ContactRequestsView.java b/ring-android/libringclient/src/main/java/cx/ring/contactrequests/ContactRequestsView.java
index 7b6de07e721eca3b6de61b30c98a2462ce52cb10..4e9e0013e0932d4c61a267586df5b00e9d5ecd83 100644
--- a/ring-android/libringclient/src/main/java/cx/ring/contactrequests/ContactRequestsView.java
+++ b/ring-android/libringclient/src/main/java/cx/ring/contactrequests/ContactRequestsView.java
@@ -21,6 +21,7 @@ package cx.ring.contactrequests;
 
 import java.util.List;
 
+import cx.ring.model.Uri;
 import cx.ring.smartlist.SmartListViewModel;
 
 public interface ContactRequestsView {
@@ -28,5 +29,5 @@ public interface ContactRequestsView {
     void updateView(List<SmartListViewModel> list);
     void updateItem(SmartListViewModel item);
 
-    void goToConversation(String accountId, String contactId);
+    void goToConversation(String accountId, Uri contactId);
 }
diff --git a/ring-android/libringclient/src/main/java/cx/ring/conversation/ConversationPresenter.java b/ring-android/libringclient/src/main/java/cx/ring/conversation/ConversationPresenter.java
index 21e5dea4000eec64563d3f2b212e6bce4725376a..23c14a6acf45d27d19cd5fd93462a256a192bf94 100644
--- a/ring-android/libringclient/src/main/java/cx/ring/conversation/ConversationPresenter.java
+++ b/ring-android/libringclient/src/main/java/cx/ring/conversation/ConversationPresenter.java
@@ -67,8 +67,7 @@ public class ConversationPresenter extends RootPresenter<ConversationView> {
     private final PreferencesService mPreferencesService;
 
     private Conversation mConversation;
-    private Uri mContactUri;
-    private String mAccountId;
+    private Uri mConversationUri;
 
     private CompositeDisposable mConversationDisposable;
     private final CompositeDisposable mVisibilityDisposable = new CompositeDisposable();
@@ -94,61 +93,50 @@ public class ConversationPresenter extends RootPresenter<ConversationView> {
         mVCardService = vCardService;
         mDeviceRuntimeService = deviceRuntimeService;
         mPreferencesService = preferencesService;
+        mCompositeDisposable.add(mVisibilityDisposable);
     }
 
-    @Override
+    /*@Override
     public void bindView(ConversationView view) {
         super.bindView(view);
         mCompositeDisposable.add(mVisibilityDisposable);
         if (mConversationDisposable == null && mConversation != null)
-            initView(mConversation, view);
-    }
+            initView(mAccountService.getAccount(mConversation.getAccountId()), mConversation, view);
+    }*/
 
-    public void init(Uri contactRingId, String accountId) {
-        Log.w(TAG, "init " + contactRingId + " " + accountId);
-        mContactUri = contactRingId;
-        mAccountId = accountId;
+    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, contactRingId, getView());
-            mCompositeDisposable.add(mConversationFacade.loadConversationHistory(account, contactRingId)
+            initContact(account, account.getByUri(mConversationUri), getView());
+            mCompositeDisposable.add(mConversationFacade.loadConversationHistory(account, conversationUri)
                     .observeOn(mUiScheduler)
-                    .subscribe(this::setConversation, e -> getView().goToHome()));
+                    .subscribe(c -> setConversation(account, c), e -> {
+                        Log.e(TAG, "Error loading conversation", e);
+                        getView().goToHome();
+                    }));
         } else {
             getView().goToHome();
             return;
         }
 
-        mCompositeDisposable.add(Observable.combineLatest(
-                mHardwareService.getConnectivityState(),
-                mAccountService.getObservableAccount(account),
-                (isConnected, a) -> isConnected || a.isRegistered())
-                .observeOn(mUiScheduler)
-                .subscribe(isOk -> {
-                    ConversationView view = getView();
-                    if (view != null) {
-                        if (!isOk)
-                            view.displayNetworkErrorPanel();
-                        else if(!account.isEnabled()) {
-                            view.displayAccountOfflineErrorPanel();
-                        }
-                        else {
-                            view.hideErrorPanel();
-                        }
-                    }
-                }));
-
         getView().setReadIndicatorStatus(showReadIndicator());
     }
 
-    private void setConversation(final Conversation conversation) {
-        if (conversation == null || mConversation == conversation)
+    private void setConversation(Account account, final Conversation conversation) {
+        Log.w(TAG, "setConversation " + conversation.getAggregateHistory().size());
+        if (mConversation == conversation)
             return;
+        if (mConversation != null)
+            mConversation.setVisible(false);
         mConversation = conversation;
         mConversationSubject.onNext(conversation);
         ConversationView view = getView();
         if (view != null)
-            initView(conversation, view);
+            initView(account, conversation, view);
     }
 
     public void pause() {
@@ -159,48 +147,38 @@ public class ConversationPresenter extends RootPresenter<ConversationView> {
     }
 
     public void resume(boolean isBubble) {
-        Log.w(TAG, "resume " + mConversation + " " + mAccountId + " " + mContactUri);
+        Log.w(TAG, "resume " + mConversationUri);
         mVisibilityDisposable.clear();
         mVisibilityDisposable.add(mConversationSubject
-                .firstOrError()
                 .subscribe(conversation -> {
                     conversation.setVisible(true);
                     updateOngoingCallView(conversation);
-                    mConversationFacade.readMessages(mAccountService.getAccount(mAccountId), conversation, !isBubble);
+                    mConversationFacade.readMessages(mAccountService.getAccount(conversation.getAccountId()), conversation, !isBubble);
                 }, e -> Log.e(TAG, "Error loading conversation", e)));
     }
 
-    private CallContact initContact(final Account account, final Uri uri,
-                                    final ConversationView view) {
-        CallContact contact;
+    private void initContact(final Account account, final Conversation conversation, final ConversationView view) {
         if (account.isJami()) {
-            String rawId = uri.getRawRingId();
-            contact = account.getContact(rawId);
-            if (contact == null) {
-                contact = account.getContactFromCache(uri);
+            Log.w(TAG, "initContact " + conversation.getUri());
+            if (conversation.isSwarm() || account.isContact(conversation)) {
+                view.switchToConversationView();
+            } else {
+                Uri uri = conversation.getUri();
                 TrustRequest req = account.getRequest(uri);
                 if (req == null) {
-                    view.switchToUnknownView(contact.getRingUsername());
+                    view.switchToUnknownView(uri.getRawUriString());
                 } else {
                     view.switchToIncomingTrustRequestView(req.getDisplayname());
                 }
-            } else {
-                view.switchToConversationView();
-            }
-            Log.w(TAG, "initContact " + contact.getUsername());
-            if (contact.getUsername() == null) {
-                mAccountService.lookupAddress(mAccountId, "", rawId);
             }
         } else {
-            contact = mContactService.findContact(account, uri);
             view.switchToConversationView();
         }
-        view.displayContact(contact);
-        return contact;
+        view.displayContact(conversation);
     }
 
-    private void initView(final Conversation c, final ConversationView view) {
-        Log.w(TAG, "initView");
+    private void initView(Account account, final Conversation c, final ConversationView view) {
+        Log.w(TAG, "initView " + c.getUri());
         if (mConversationDisposable == null) {
             mConversationDisposable = new CompositeDisposable();
             mCompositeDisposable.add(mConversationDisposable);
@@ -208,17 +186,52 @@ public class ConversationPresenter extends RootPresenter<ConversationView> {
         mConversationDisposable.clear();
         view.hideNumberSpinner();
 
-        Account account = mAccountService.getAccount(mAccountId);
+        if (account.isJami() && !c.isSwarm()) {
+            String accountId = account.getAccountID();
+            mConversationDisposable.add(c.getContact().getConversationUri()
+                    .observeOn(mUiScheduler)
+                    .subscribe(uri -> init(uri, accountId)));
+        }
 
+        mConversationDisposable.add(Observable.combineLatest(
+                mHardwareService.getConnectivityState(),
+                mAccountService.getObservableAccount(account),
+                (isConnected, a) -> isConnected || a.isRegistered())
+                .observeOn(mUiScheduler)
+                .subscribe(isOk -> {
+                    ConversationView v = getView();
+                    if (v != null) {
+                        if (!isOk)
+                            v.displayNetworkErrorPanel();
+                        else if(!account.isEnabled()) {
+                            v.displayAccountOfflineErrorPanel();
+                        }
+                        else {
+                            v.hideErrorPanel();
+                        }
+                    }
+                }));
 
         mConversationDisposable.add(c.getSortedHistory()
+                .observeOn(mUiScheduler)
                 .subscribe(view::refreshView, e -> Log.e(TAG, "Can't update element", e)));
         mConversationDisposable.add(c.getCleared()
                 .observeOn(mUiScheduler)
                 .subscribe(view::refreshView, e -> Log.e(TAG, "Can't update elements", e)));
-        mConversationDisposable.add(mContactService.getLoadedContact(c.getAccountId(), c.getContact())
+
+        mConversationDisposable.add(c.getContactUpdates()
+                .switchMap(contacts -> Observable.merge(mContactService.observeLoadedContact(c.getAccountId(), contacts, true)))
+                .observeOn(mUiScheduler)
+                .subscribe(contact -> {
+                    ConversationView v = getView();
+                    if (v != null)
+                        v.updateContact(contact);
+                }));
+
+        mConversationDisposable.add(mContactService.getLoadedContact(c.getAccountId(), c.getContacts(), true)
                 .observeOn(mUiScheduler)
-                .subscribe(contact -> initContact(account, mContactUri, view), e -> Log.e(TAG, "Can't get contact", e)));
+                .subscribe(contact -> initContact(account, c, view), e -> Log.e(TAG, "Can't get contact", e)));
+
         mConversationDisposable.add(c.getUpdatedElements()
                 .observeOn(mUiScheduler)
                 .subscribe(elementTuple -> {
@@ -234,6 +247,7 @@ public class ConversationPresenter extends RootPresenter<ConversationView> {
                             break;
                     }
                 }, e -> Log.e(TAG, "Can't update element", e)));
+
         if (showTypingIndicator()) {
             mConversationDisposable.add(c.getComposingStatus()
                     .observeOn(mUiScheduler)
@@ -251,17 +265,21 @@ public class ConversationPresenter extends RootPresenter<ConversationView> {
 
         Log.e(TAG, "getLocationUpdates subscribe");
         mConversationDisposable.add(account
-                .getLocationUpdates(c.getContact().getPrimaryUri())
+                .getLocationUpdates(c.getUri())
                 .observeOn(mUiScheduler)
                 .subscribe(u -> {
                     Log.e(TAG, "getLocationUpdates: update");
-                    getView().showMap(c.getAccountId(), c.getContact().getPrimaryUri().getUri(), false);
+                    getView().showMap(c.getAccountId(), c.getUri().getUri(), false);
                 }));
     }
 
+    public void loadMore() {
+        mConversationFacade.loadMore(mConversation);
+    }
+
     public void openContact() {
         if (mConversation != null)
-            getView().goToContactActivity(mAccountId, mConversation.getContact().getPrimaryNumber());
+            getView().goToContactActivity(mConversation.getAccountId(), mConversation.getUri());
     }
 
     public void sendTextMessage(String message) {
@@ -269,8 +287,8 @@ public class ConversationPresenter extends RootPresenter<ConversationView> {
             return;
         }
         Conference conference = mConversation.getCurrentCall();
-        if (conference == null || !conference.isOnGoing()) {
-            mConversationFacade.sendTextMessage(mAccountId, mConversation, mContactUri, message).subscribe();
+        if (mConversation.isSwarm() || conference == null || !conference.isOnGoing()) {
+            mConversationFacade.sendTextMessage(mConversation, mConversationUri, message).subscribe();
         } else {
             mConversationFacade.sendTextMessage(mConversation, conference, message);
         }
@@ -281,7 +299,9 @@ public class ConversationPresenter extends RootPresenter<ConversationView> {
     }
 
     public void sendFile(File file) {
-        mConversationFacade.sendFile(mAccountId, mContactUri, file).subscribe();
+        if (mConversation ==  null)
+            return;
+        mConversationFacade.sendFile(mConversation, mConversationUri, file).subscribe();
     }
 
     /**
@@ -293,25 +313,37 @@ public class ConversationPresenter extends RootPresenter<ConversationView> {
     public void saveFile(Interaction interaction) {
         DataTransfer transfer = (DataTransfer) interaction;
         String fileAbsolutePath = getDeviceRuntimeService().
-                getConversationPath(transfer.getPeerId(), transfer.getStoragePath())
+                getConversationPath(mConversation.getUri().getRawRingId(), transfer.getStoragePath())
                 .getAbsolutePath();
         getView().startSaveFile(transfer, fileAbsolutePath);
     }
 
     public void shareFile(Interaction interaction) {
         DataTransfer file = (DataTransfer) interaction;
-        File path = getDeviceRuntimeService().getConversationPath(file.getPeerId(), file.getStoragePath());
+        File path = getDeviceRuntimeService().getConversationPath(mConversation.getUri().getRawRingId(), file.getStoragePath());
         getView().shareFile(path);
     }
 
     public void openFile(Interaction interaction) {
         DataTransfer file = (DataTransfer) interaction;
-        File path = getDeviceRuntimeService().getConversationPath(file.getPeerId(), file.getStoragePath());
+        File path = getDeviceRuntimeService().getConversationPath(mConversation.getUri().getRawRingId(), file.getStoragePath());
         getView().openFile(path);
     }
 
+    public void acceptFile(DataTransfer transfer) {
+        if (!getDeviceRuntimeService().hasWriteExternalStoragePermission()) {
+            getView().askWriteExternalStoragePermission();
+            return;
+        }
+        getView().acceptFile(mConversation.getAccountId(), mConversationUri, transfer);
+    }
+
+    public void refuseFile(DataTransfer transfer) {
+        getView().refuseFile(mConversation.getAccountId(), mConversationUri, transfer);
+    }
+
     public void deleteConversationItem(Interaction element) {
-        mConversationFacade.deleteConversationItem(element);
+        mConversationFacade.deleteConversationItem(mConversation, element);
     }
 
     public void cancelMessage(Interaction message) {
@@ -319,16 +351,15 @@ public class ConversationPresenter extends RootPresenter<ConversationView> {
     }
 
     private void sendTrustRequest() {
-        final String accountId = mAccountId;
-        final Uri contactId = mContactUri;
-        CallContact contact = mContactService.findContact(mAccountService.getAccount(accountId), contactId);
+        //final Uri contactId = mConversationUri;
+        CallContact contact = mConversation.getContact();//mAccountService.getAccount(accountId).getContactFromCache(contactId);
         if (contact != null) {
             contact.setStatus(CallContact.Status.REQUEST_SENT);
         }
-        mVCardService.loadSmallVCard(accountId, VCardService.MAX_SIZE_REQUEST)
+        mVCardService.loadSmallVCard(mConversation.getAccountId(), VCardService.MAX_SIZE_REQUEST)
                 .subscribeOn(Schedulers.computation())
-                .subscribe(vCard -> mAccountService.sendTrustRequest(accountId, contactId.getRawRingId(), Blob.fromString(VCardUtils.vcardToString(vCard))),
-                        e -> mAccountService.sendTrustRequest(accountId, contactId.getRawRingId(), null));
+                .subscribe(vCard -> mAccountService.sendTrustRequest(mConversation, contact.getUri(), Blob.fromString(VCardUtils.vcardToString(vCard))),
+                        e -> mAccountService.sendTrustRequest(mConversation, contact.getUri(), null));
     }
 
     public void clickOnGoingPane() {
@@ -358,7 +389,7 @@ public class ConversationPresenter extends RootPresenter<ConversationView> {
                                 && conf.getParticipants().get(0).getCallStatus() != SipCall.CallStatus.FAILURE) {
                             view.goToCallActivity(conf.getId());
                         } else {
-                            view.goToCallActivityWithResult(mAccountId, mContactUri.getRawUriString(), audioOnly);
+                            view.goToCallActivityWithResult(mConversation.getAccountId(), mConversation.getUri(), mConversation.getContact().getUri(), audioOnly);
                         }
                     }
                 }));
@@ -374,22 +405,19 @@ public class ConversationPresenter extends RootPresenter<ConversationView> {
     }
 
     public void onBlockIncomingContactRequest() {
-        String accountId = mAccountId == null ? mAccountService.getCurrentAccount().getAccountID() : mAccountId;
-        mConversationFacade.discardRequest(accountId, mContactUri);
-        mAccountService.removeContact(accountId, mContactUri.getHost(), true);
+        mConversationFacade.discardRequest(mConversation.getAccountId(), mConversationUri);
+        mAccountService.removeContact(mConversation.getAccountId(), mConversationUri.getHost(), true);
 
         getView().goToHome();
     }
 
     public void onRefuseIncomingContactRequest() {
-        String accountId = mAccountId == null ? mAccountService.getCurrentAccount().getAccountID() : mAccountId;
-
-        mConversationFacade.discardRequest(accountId, mContactUri);
+        mConversationFacade.discardRequest(mConversation.getAccountId(), mConversationUri);
         getView().goToHome();
     }
 
     public void onAcceptIncomingContactRequest() {
-        mConversationFacade.acceptRequest(mAccountId, mContactUri);
+        mConversationFacade.acceptRequest(mConversation.getAccountId(), mConversationUri);
         getView().switchToConversationView();
     }
 
@@ -422,7 +450,7 @@ public class ConversationPresenter extends RootPresenter<ConversationView> {
     }
 
     public void shareLocation() {
-        getView().startShareLocation(mAccountId, mContactUri.getUri());
+        getView().startShareLocation(mConversation.getAccountId(), mConversationUri.getUri());
     }
 
     public void showPluginListHandlers() {
@@ -430,14 +458,14 @@ public class ConversationPresenter extends RootPresenter<ConversationView> {
     }
 
     public Tuple<String, String> getPath() {
-        return new Tuple<>(mAccountId, mContactUri.getUri());
+        return new Tuple<>(mConversation.getAccountId(), mConversationUri.getUri());
     }
 
     public void onComposingChanged(boolean hasMessage) {
         if (mConversation == null || !showTypingIndicator()) {
             return;
         }
-        mConversationFacade.setIsComposing(mAccountId, mContactUri, hasMessage);
+        mConversationFacade.setIsComposing(mConversation.getAccountId(), mConversationUri, hasMessage);
     }
 
     public boolean showTypingIndicator() {
diff --git a/ring-android/libringclient/src/main/java/cx/ring/conversation/ConversationView.java b/ring-android/libringclient/src/main/java/cx/ring/conversation/ConversationView.java
index ee08f4645185a8b01c8f65174b09f0f39ee5409c..47e8ccc50c833dcdb1b0f2b2ee29292b264f66ca 100644
--- a/ring-android/libringclient/src/main/java/cx/ring/conversation/ConversationView.java
+++ b/ring-android/libringclient/src/main/java/cx/ring/conversation/ConversationView.java
@@ -37,7 +37,9 @@ public interface ConversationView extends BaseView {
 
     void scrollToEnd();
 
-    void displayContact(CallContact contact);
+    void updateContact(CallContact contact);
+
+    void displayContact(Conversation conversation);
 
     void displayOnGoingCallPane(boolean display);
 
@@ -55,9 +57,9 @@ public interface ConversationView extends BaseView {
 
     void goToCallActivity(String conferenceId);
 
-    void goToCallActivityWithResult(String accountId, String contactRingId, boolean audioOnly);
+    void goToCallActivityWithResult(String accountId, Uri conversationUri, Uri contactUri, boolean audioOnly);
 
-    void goToContactActivity(String accountId, String contactRingId);
+    void goToContactActivity(String accountId, Uri uri);
 
     void switchToUnknownView(String name);
 
@@ -69,8 +71,9 @@ public interface ConversationView extends BaseView {
 
     void openFilePicker();
 
+    void acceptFile(String accountId, Uri conversationUri, DataTransfer transfer);
+    void refuseFile(String accountId, Uri conversationUri, DataTransfer transfer);
     void shareFile(File path);
-
     void openFile(File path);
 
     void addElement(Interaction e);
@@ -98,4 +101,6 @@ public interface ConversationView extends BaseView {
 
     void setReadIndicatorStatus(boolean show);
 
+    void updateLastRead(String last);
+
 }
diff --git a/ring-android/libringclient/src/main/java/cx/ring/facades/ConversationFacade.java b/ring-android/libringclient/src/main/java/cx/ring/facades/ConversationFacade.java
index 908a987118988a217c9a00601802a83898b79e15..30e416ac20ec840ff7adc4bbe934340cce1380aa 100644
--- a/ring-android/libringclient/src/main/java/cx/ring/facades/ConversationFacade.java
+++ b/ring-android/libringclient/src/main/java/cx/ring/facades/ConversationFacade.java
@@ -22,6 +22,7 @@ package cx.ring.facades;
 
 import java.io.File;
 import java.util.ArrayList;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
 import java.util.NavigableMap;
@@ -117,7 +118,7 @@ public class ConversationFacade {
 
         mDisposableBag.add(mAccountService.getIncomingRequests()
                 .concatMapSingle(r -> getAccountSubject(r.getAccountId()))
-                .subscribe(a -> mNotificationService.showIncomingTrustRequestNotification(a),
+                .subscribe(mNotificationService::showIncomingTrustRequestNotification,
                         e -> Log.e(TAG, "Error showing contact request")));
 
         mDisposableBag.add(mAccountService
@@ -129,6 +130,10 @@ public class ConversationFacade {
                         }))
                 .subscribe(this::parseNewMessage,
                         e -> Log.e(TAG, "Error adding text message", e)));
+        mDisposableBag.add(mAccountService
+                .getIncomingSwarmMessages()
+                .subscribe(this::parseNewMessage,
+                        e -> Log.e(TAG, "Error adding text message", e)));
 
         mDisposableBag.add(mAccountService.getLocationUpdates()
                 .concatMapSingle(location -> getAccountSubject(location.getAccount())
@@ -158,15 +163,15 @@ public class ConversationFacade {
         mDisposableBag.add(mAccountService
                 .getMessageStateChanges()
                 .concatMapSingle(txt -> getAccountSubject(txt.getAccount())
-                        .map(a -> a.getByUri(txt.getConversation().getParticipant()))
-                        .doOnSuccess(conv -> conv.updateTextMessage(txt)))
+                        .map(a -> txt.getConversation() == null ? a.getSwarm(txt.getConversationId()) : a.getByUri(txt.getConversation().getParticipant()))
+                        .doOnSuccess(conversation -> conversation.updateTextMessage(txt)))
                 .subscribe(c -> {
                 }, e -> Log.e(TAG, "Error updating text message", e)));
 
         mDisposableBag.add(mAccountService
                 .getDataTransfers()
                 .subscribe(this::handleDataTransferEvent,
-                        e -> Log.e(TAG, "Error adding data transfer")));
+                        e -> Log.e(TAG, "Error adding data transfer", e)));
     }
 
     public Observable<Conversation> getUpdatedConversation() {
@@ -193,56 +198,68 @@ public class ConversationFacade {
                 .switchMap(Account::getConversationsSubject);
     }
 
-    public void readMessages(String accountId, Uri contact) {
+    public String readMessages(String accountId, Uri contact) {
         Account account = mAccountService.getAccount(accountId);
-        if (account != null)
-            readMessages(account, account.getByUri(contact), true);
+        return account != null ?
+             readMessages(account, account.getByUri(contact), true) : null;
     }
 
-    public void readMessages(Account account, Conversation conversation, boolean cancelNotification) {
+    public String readMessages(Account account, Conversation conversation, boolean cancelNotification) {
         if (conversation != null) {
             String lastMessage = readMessages(conversation);
             if (lastMessage != null) {
                 account.refreshed(conversation);
                 if (mPreferencesService.getSettings().isAllowReadIndicator()) {
-                    mAccountService.setMessageDisplayed(account.getAccountID(), conversation.getContact().getPrimaryNumber(), lastMessage);
+                    mAccountService.setMessageDisplayed(account.getAccountID(), conversation.getUri().getRawRingId(), lastMessage);
                 }
                 if (cancelNotification) {
-                    mNotificationService.cancelTextNotification(conversation.getContact().getPrimaryUri());
+                    mNotificationService.cancelTextNotification(account.getAccountID(), conversation.getUri());
                 }
             }
+            return lastMessage;
         }
+        return null;
     }
 
     private String readMessages(Conversation conversation) {
         String lastRead = null;
-        NavigableMap<Long, Interaction> messages = conversation.getRawHistory();
-        for (Interaction e : messages.descendingMap().values()) {
-            if (!(e.getType().equals(InteractionType.TEXT)))
-                continue;
-            if (e.isRead()) {
-                break;
+        if (conversation.isSwarm()) {
+            lastRead = conversation.readMessages();
+            if (lastRead != null)
+                mHistoryService.setMessageRead(conversation.getAccountId(), conversation.getUri(), lastRead);
+        } else {
+            NavigableMap<Long, Interaction> messages = conversation.getRawHistory();
+            for (Interaction e : messages.descendingMap().values()) {
+                if (!(e.getType().equals(InteractionType.TEXT)))
+                    continue;
+                if (e.isRead()) {
+                    break;
+                }
+                e.read();
+                Long did = e.getDaemonId();
+                if (lastRead == null && did != null && did != 0L)
+                    lastRead = Long.toString(did, 16);
+                mHistoryService.updateInteraction(e, conversation.getAccountId()).subscribe();
             }
-            e.read();
-            Long did = e.getDaemonId();
-            if (lastRead == null && did != null && did != 0L)
-                lastRead = Long.toString(did, 16);
-            mHistoryService.updateInteraction(e, conversation.getAccountId()).subscribe();
         }
         return lastRead;
     }
 
-    public Single<TextMessage> sendTextMessage(String account, Conversation c, Uri to, String txt) {
-        return mCallService.sendAccountTextMessage(account, to.getRawUriString(), txt)
+    public Completable sendTextMessage(Conversation c, Uri to, String txt) {
+        if (c.isSwarm()) {
+            mAccountService.sendConversationMessage(c.getAccountId(), c.getUri(), txt);
+            return Completable.complete();
+        }
+        return mCallService.sendAccountTextMessage(c.getAccountId(), to.getRawUriString(), txt)
                 .map(id -> {
-                    TextMessage message = new TextMessage(null, account, Long.toHexString(id), c, txt);
+                    TextMessage message = new TextMessage(null, c.getAccountId(), Long.toHexString(id), c, txt);
                     if (c.isVisible())
                         message.read();
-                    mHistoryService.insertInteraction(account, c, message).subscribe();
+                    mHistoryService.insertInteraction(c.getAccountId(), c, message).subscribe();
                     c.addTextMessage(message);
-                    mAccountService.getAccount(account).conversationUpdated(c);
+                    mAccountService.getAccount(c.getAccountId()).conversationUpdated(c);
                     return message;
-                });
+                }).ignoreElement();
     }
 
     public void sendTextMessage(Conversation c, Conference conf, String txt) {
@@ -253,60 +270,56 @@ public class ConversationFacade {
         c.addTextMessage(message);
     }
 
-    public void setIsComposing(String accountId, Uri contactUri, boolean isComposing) {
-        mCallService.setIsComposing(accountId, contactUri.getRawUriString(), isComposing);
+    public void setIsComposing(String accountId, Uri conversationUri, boolean isComposing) {
+        mCallService.setIsComposing(accountId, conversationUri.getRawRingId(), isComposing);
     }
 
-    public Completable sendFile(String account, Uri to, File file) {
-        return Completable.fromAction(() -> {
-            if (file == null) {
-                return;
+    public Completable sendFile(Conversation conversation, Uri to, File file) {
+        return Single.fromCallable(() -> {
+            if (file == null || !file.exists() || !file.canRead()) {
+                Log.w(TAG, "sendFile: file not found or not readable: " + file);
+                return null;
             }
 
-            // check file
-            if (!file.exists()) {
-                Log.w(TAG, "sendFile: file not found");
-                return;
+            DataTransfer transfer = new DataTransfer(conversation, to.getRawRingId(), conversation.getAccountId(), file.getName(), true, file.length(), 0, 0L);
+            if (conversation.isSwarm()) {
+                transfer.setSwarmInfo(conversation.getUri().getRawRingId(), null, null);
+            } else {
+                mHistoryService.insertInteraction(conversation.getAccountId(), conversation, transfer).blockingAwait();
             }
 
-            if (!file.canRead()) {
-                Log.w(TAG, "sendFile: file not readable");
-                return;
-            }
-            String peerId = to.getUri();
-            Conversation conversation = mAccountService.getAccount(account).getByUri(to);
-            DataTransfer transfer = new DataTransfer(conversation, account, file.getName(), true, file.length(), 0, 0L);
-            mHistoryService.insertInteraction(account, conversation, transfer).blockingAwait();
-            File dest = mDeviceRuntimeService.getConversationPath(peerId, transfer.getStoragePath());
+            File dest = mDeviceRuntimeService.getConversationPath(conversation.getUri().getRawRingId(), transfer.getStoragePath());
             if (!FileUtils.moveFile(file, dest)) {
                 Log.e(TAG, "sendFile: can't move file to " + dest);
-                return;
+                return null;
             }
 
-            // send file
-            mAccountService.sendFile(transfer, dest);
-        }).subscribeOn(Schedulers.io());
+            transfer.destination = dest;
+            return transfer;
+        })
+                .subscribeOn(Schedulers.io())
+                .flatMapCompletable(mAccountService::sendFile);
     }
 
 
-    public void deleteConversationItem(Interaction element) {
+    public void deleteConversationItem(Conversation conversation, Interaction element) {
         if (element.getType() == InteractionType.DATA_TRANSFER) {
             DataTransfer transfer = (DataTransfer) element;
             if (transfer.getStatus() == InteractionStatus.TRANSFER_ONGOING) {
-                mAccountService.cancelDataTransfer(transfer.getDaemonId());
+                mAccountService.cancelDataTransfer(conversation.getAccountId(), conversation.getUri().getRawRingId(), transfer.getDaemonId());
             } else {
-                File file = mDeviceRuntimeService.getConversationPath(transfer.getPeerId(), transfer.getStoragePath());
+                File file = mDeviceRuntimeService.getConversationPath(conversation.getUri().getRawRingId(), transfer.getStoragePath());
                 mDisposableBag.add(Completable.mergeArrayDelayError(
                         mHistoryService.deleteInteraction(element.getId(), element.getAccount()),
-                        Completable.fromAction(file::delete).subscribeOn(Schedulers.io()))
-                        .andThen(startConversation(transfer.getAccount(), new Uri(transfer.getConversation().getParticipant())))
-                        .subscribe(c -> c.removeInteraction(transfer),
+                        Completable.fromAction(file::delete)
+                                .subscribeOn(Schedulers.io()))
+                        .subscribe(() -> conversation.removeInteraction(transfer),
                                 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(), new Uri(element.getConversation().getParticipant())))
+                    .andThen(startConversation(element.getAccount(), Uri.fromString(element.getConversation().getParticipant())))
                     .subscribe(c -> c.removeInteraction(element),
                             e -> Log.e(TAG, "Can't delete message", e)));
         }
@@ -315,7 +328,7 @@ public class ConversationFacade {
     public void cancelMessage(Interaction message) {
         mDisposableBag.add(Completable.mergeArrayDelayError(
                 mCallService.cancelMessage(message.getAccount(), message.getId()).subscribeOn(Schedulers.io()))
-                .andThen(startConversation(message.getAccount(), new Uri(message.getConversation().getParticipant())))
+                .andThen(startConversation(message.getAccount(), Uri.fromString(message.getConversation().getParticipant())))
                 .subscribe(c -> c.removeInteraction(message),
                         e -> Log.e(TAG, "Can't cancel message sending", e)));
     }
@@ -340,20 +353,26 @@ public class ConversationFacade {
      * Loads history for a specific conversation from cache or database
      *
      * @param account    the user account
-     * @param contactUri the conversation participant
+     * @param conversationUri the conversation
      * @return a conversation single
      */
-    public Single<Conversation> loadConversationHistory(final Account account, final Uri contactUri) {
-        Conversation conversation = account.getByUri(contactUri);
+    public Single<Conversation> loadConversationHistory(final Account account, final Uri conversationUri) {
+        Conversation conversation = account.getByUri(conversationUri);
         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) {
                 return Single.just(conversation);
             }
             Single<Conversation> ret = conversation.getLoaded();
             if (ret == null) {
-                ret = getConversationHistory(account, conversation);
+                ret = getConversationHistory(conversation);
                 conversation.setLoaded(ret);
             }
             return ret;
@@ -361,23 +380,36 @@ public class ConversationFacade {
     }
 
     private Observable<SmartListViewModel> observeConversation(Account account, Conversation conversation, boolean hasPresence) {
-        return account.getConversationSubject()
+        return Observable.merge(account.getConversationSubject()
+                .filter(c -> c == conversation)
+                .startWith(conversation),
+                mContactService
+                        .observeContact(conversation.getAccountId(), conversation.getContacts(), hasPresence))
+        .map(e -> new SmartListViewModel(conversation, hasPresence));
+        /*return account.getConversationSubject()
                 .filter(c -> c == conversation)
                 .startWith(conversation)
                 .switchMap(c -> mContactService
-                        .observeContact(c.getAccountId(), c.getContact(), hasPresence)
-                        .map(contact -> new SmartListViewModel(c, hasPresence)));
+                        .observeContact(c.getAccountId(), c.getContacts(), hasPresence)
+                        .map(contact -> new SmartListViewModel(c, hasPresence)));*/
     }
     public Observable<List<Observable<SmartListViewModel>>> getSmartList(Observable<Account> currentAccount, boolean hasPresence) {
         return currentAccount.switchMap(account -> account.getConversationsSubject()
                 .switchMapSingle(conversations -> Observable.fromIterable(conversations)
-                        .map(conv -> observeConversation(account, conv, hasPresence))
+                        .map(conversation -> observeConversation(account, conversation, hasPresence))
+                        .toList()));
+    }
+    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 SmartListViewModel(conversation,false))
                         .toList()));
     }
     public Observable<List<Observable<SmartListViewModel>>> getPendingList(Observable<Account> currentAccount) {
         return currentAccount.switchMap(account -> account.getPendingSubject()
                 .switchMapSingle(conversations -> Observable.fromIterable(conversations)
-                        .map(conv -> observeConversation(account, conv, false))
+                        .map(conversation -> observeConversation(account, conversation, false))
                         .toList()));
     }
 
@@ -387,16 +419,19 @@ public class ConversationFacade {
     public Observable<List<Observable<SmartListViewModel>>> getPendingList() {
         return getPendingList(mAccountService.getCurrentAccountSubject());
     }
+    public Observable<List<SmartListViewModel>> getContactList() {
+        return getContactList(mAccountService.getCurrentAccountSubject());
+    }
 
     private Single<List<Observable<SmartListViewModel>>> getSearchResults(Account account, String query) {
-        Uri uri = new Uri(query);
+        Uri uri = Uri.fromString(query);
         if (account.isSip()) {
             CallContact contact = account.getContactFromCache(uri);
             return mContactService.loadContactData(contact, account.getAccountID())
-                    .andThen(Single.just(Collections.singletonList(Observable.just(new SmartListViewModel(account.getAccountID(), contact, null)))));
-        } else if (uri.isRingId()) {
+                    .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 SmartListViewModel(account.getAccountID(), contact, 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);
@@ -431,7 +466,7 @@ public class ConversationFacade {
                             newList.add(SmartListViewModel.TITLE_CONVERSATIONS);
                             int nRes = 0;
                             for (Conversation conversation : conversations) {
-                                if (conversation.getContact().matches(lq)) {
+                                if (conversation.matches(lq)) {
                                     newList.add(observeConversation(account, conversation, hasPresence));
                                     nRes++;
                                 }
@@ -470,17 +505,16 @@ public class ConversationFacade {
     /**
      * Loads a conversation's history from the database
      *
-     * @param account      the user account
      * @param conversation a conversation object with a valid conversation ID
      * @return a conversation single
      */
-    private Single<Conversation> getConversationHistory(final Account account, final Conversation conversation) {
-        Log.d(TAG, "getConversationHistory()");
+    private Single<Conversation> getConversationHistory(final Conversation conversation) {
+        Log.d(TAG, "getConversationHistory() " + conversation.getAccountId() + " " + conversation.getUri());
 
-        return mHistoryService.getConversationHistory(account.getAccountID(), conversation.getId())
+        return mHistoryService.getConversationHistory(conversation.getAccountId(), conversation.getId())
                 .map(loadedConversation -> {
-                    if (loadedConversation.isEmpty())
-                        return null;
+                    /*if (loadedConversation.isEmpty())
+                        return conversation;*/
                     conversation.clearHistory(true);
                     conversation.setHistory(loadedConversation);
                     return conversation;
@@ -513,7 +547,7 @@ public class ConversationFacade {
     }
 
     public void updateTextNotifications(String accountId, List<Conversation> conversations) {
-        Log.d(TAG, "updateTextNotifications()");
+        Log.d(TAG, "updateTextNotifications() " + accountId + " " + conversations.size());
 
         for (Conversation conversation : conversations) {
             mNotificationService.showTextNotification(accountId, conversation);
@@ -522,9 +556,15 @@ public class ConversationFacade {
 
     private void parseNewMessage(final TextMessage txt) {
         if (txt.isRead()) {
-            mHistoryService.updateInteraction(txt, txt.getAccount()).subscribe();
+            if (txt.getMessageId() == null) {
+                mHistoryService.updateInteraction(txt, txt.getAccount()).subscribe();
+            }
             if (mPreferencesService.getSettings().isAllowReadIndicator()) {
-                mAccountService.setMessageDisplayed(txt.getAccount(), txt.getAuthor(), Long.toString(txt.getDaemonId(), 16));
+                if (txt.getMessageId() != null) {
+                    mAccountService.setMessageDisplayed(txt.getAccount(), txt.getConversationId(), txt.getMessageId());
+                } else {
+                    mAccountService.setMessageDisplayed(txt.getAccount(), txt.getAuthor(), Long.toString(txt.getDaemonId(), 16));
+                }
             }
         }
         getAccountSubject(txt.getAccount())
@@ -551,10 +591,10 @@ public class ConversationFacade {
         Conversation conversation = mAccountService.getAccount(transfer.getAccount()).onDataTransferEvent(transfer);
         if (transfer.getStatus() == InteractionStatus.TRANSFER_CREATED && !transfer.isOutgoing()) {
             if (transfer.canAutoAccept(mPreferencesService.getMaxFileAutoAccept(transfer.getAccount()))) {
-                mAccountService.acceptFileTransfer(transfer);
+                mAccountService.acceptFileTransfer(conversation, transfer);
             }
         }
-        mNotificationService.handleDataTransferNotification(transfer, conversation.getContact(), conversation.isVisible());
+        mNotificationService.handleDataTransferNotification(transfer, conversation, conversation.isVisible());
     }
 
     private void onConfStateChange(Conference conference) {
@@ -568,8 +608,19 @@ public class ConversationFacade {
         mHardwareService.updateAudioState(newState, incomingCall, !call.isAudioOnly());
 
         Account account = mAccountService.getAccount(call.getAccount());
+        if (account == null)
+            return;
         CallContact contact = call.getContact();
-        Conversation conversation = (contact == null || account == null) ? null : account.getByUri(contact.getPrimaryUri());
+        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));
         Conference conference = null;
         if (conversation != null) {
             conference = conversation.getConference(call.getDaemonIdString());
@@ -607,7 +658,8 @@ public class ConversationFacade {
             if (newState == SipCall.CallStatus.HUNGUP || call.getTimestampEnd() == 0) {
                 call.setTimestampEnd(now);
             }
-            if (conversation != null && conference.removeParticipant(call)) {
+            if (conference != null && conference.removeParticipant(call) && !conversation.isSwarm()) {
+                Log.w(TAG, "Adding call history for conversation " + conversation.getUri());
                 mHistoryService.insertInteraction(account.getAccountID(), conversation, call).subscribe();
                 conversation.addCall(call);
                 if (call.isIncoming() && call.isMissed()) {
@@ -622,22 +674,22 @@ public class ConversationFacade {
         }
     }
 
-    public Single<SipCall> placeCall(String accountId, String number, boolean video) {
-        String rawId = new Uri(number).getRawRingId();
+    public Single<SipCall> placeCall(String accountId, Uri contactUri, boolean video) {
+        //String rawId = contactUri.getRawRingId();
         return getAccountSubject(accountId).flatMap(account -> {
-            CallContact contact = account.getContact(rawId);
-            if (contact == null)
-                mAccountService.addContact(accountId, rawId);
-            return mCallService.placeCall(rawId, number, video);
+            //CallContact contact = account.getContact(rawId);
+            //if (contact == null)
+            //    mAccountService.addContact(accountId, rawId);
+            return mCallService.placeCall(accountId, null, contactUri, video);
         });
     }
 
-    public void cancelFileTransfer(long id) {
-        mAccountService.cancelDataTransfer(id);
+    public void cancelFileTransfer(String accountId, Uri conversationId, long id) {
+        mAccountService.cancelDataTransfer(accountId, conversationId.isSwarm() ? conversationId.getRawRingId() : "", id);
         mNotificationService.removeTransferNotification(id);
-        DataTransfer transfer = mAccountService.getDataTransfer(id);
+        DataTransfer transfer = mAccountService.getAccount(accountId).getDataTransfer(id);
         if (transfer != null)
-            deleteConversationItem(transfer);
+            deleteConversationItem((Conversation) transfer.getConversation(), transfer);
     }
 
     public Completable removeConversation(String accountId, Uri contact) {
@@ -649,4 +701,21 @@ public class ConversationFacade {
                     mAccountService.removeContact(accountId, contact.getRawRingId(), false);
                 });
     }
+
+    public Single<Conversation> createConversation(String accountId, Collection<CallContact> currentSelection) {
+        List<String> contactIds = new ArrayList<>(currentSelection.size());
+        for (CallContact 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/cx/ring/model/Account.java b/ring-android/libringclient/src/main/java/cx/ring/model/Account.java
index 5f6102b11a9a6e498dc611a5030c87b176974250..bcdbd9b4964ec1a7970347747edd8d0127080c1d 100644
--- a/ring-android/libringclient/src/main/java/cx/ring/model/Account.java
+++ b/ring-android/libringclient/src/main/java/cx/ring/model/Account.java
@@ -65,6 +65,8 @@ public class Account {
     private final Map<String, CallContact> mContacts = new HashMap<>();
     private final Map<String, TrustRequest> mRequests = new HashMap<>();
     private final Map<String, CallContact> mContactCache = new HashMap<>();
+    private final Map<String, Conversation> swarmConversations = new HashMap<>();
+    private final HashMap<Long, DataTransfer> mDataTransfers = new HashMap<>();
 
     private final Map<String, Conversation> conversations = new HashMap<>();
     private final Map<String, Conversation> pending = new HashMap<>();
@@ -83,16 +85,63 @@ public class Account {
     private final Subject<List<Conversation>> pendingSubject = BehaviorSubject.create();
     private final Subject<Integer> unreadConversationsSubject = BehaviorSubject.create();
     private final Subject<Integer> unreadPendingSubject = BehaviorSubject.create();
-    private final Observable<Integer> unreadConversationsCount = unreadConversationsSubject.distinct();
-    private final Observable<Integer> unreadPendingCount = unreadPendingSubject.distinct();
+    private final Observable<Integer> unreadConversationsCount = unreadConversationsSubject.distinctUntilChanged();
+    private final Observable<Integer> unreadPendingCount = unreadPendingSubject.distinctUntilChanged();
 
     private final BehaviorSubject<Collection<CallContact>> contactListSubject = BehaviorSubject.create();
-    private final BehaviorSubject<Collection<TrustRequest>> trustRequestsSubject = BehaviorSubject.create();
 
     public boolean canSearch() {
         return !StringUtils.isEmpty(getDetail(ConfigKey.MANAGER_URI));
     }
 
+    public boolean isContact(Conversation conversation) {
+        CallContact contact = conversation.getContact();
+        return contact != null && getContact(contact.getUri().getRawRingId()) != null;
+    }
+
+    public void conversationStarted(Conversation conversation) {
+        Log.w(TAG, "conversationStarted " + conversation.getAccountId() + " " + conversation.getUri() + " " + conversation.isSwarm() + " " + conversation.getContacts().size());
+        synchronized (conversations) {
+            if (conversation.isSwarm() && conversation.getMode() == Conversation.Mode.OneToOne) {
+                CallContact contact = conversation.getContact();
+                String key = contact.getUri().getUri();
+                Conversation removed = cache.remove(key);
+                conversations.remove(key);
+                //Conversation contactConversation = getByUri(contact.getPrimaryUri());
+                //Log.w(TAG, "conversationStarted " + conversation.getAccountId() + " contact " + key + " " + removed);
+                /*if (contactConversation != null) {
+                    conversations.remove(contactConversation.getUri().getUri());
+                }*/
+                contact.setConversationUri(conversation.getUri());
+            }
+            conversations.put(conversation.getUri().getUri(), conversation);
+        }
+        conversationChanged();
+    }
+    public Conversation getSwarm(String conversationId) {
+        return swarmConversations.get(conversationId);
+    }
+
+    public Conversation newSwarm(String conversationId, Conversation.Mode mode) {
+        Conversation c = swarmConversations.get(conversationId);
+        if (c == null) {
+            c = new Conversation(accountID, new Uri(Uri.SWARM_SCHEME, conversationId), mode);
+            swarmConversations.put(conversationId, c);
+        }
+        return c;
+    }
+
+    public void removeSwarm(String conversationId) {
+        Conversation conversation = swarmConversations.remove(conversationId);
+        if (conversation != null) {
+            synchronized (conversations) {
+                conversations.remove(conversation.getUri().getUri());
+            }
+            conversationChanged();
+        }
+    }
+
+
     public static class ContactLocation {
         public double latitude;
         public double longitude;
@@ -140,7 +189,7 @@ public class Account {
         conversationsSubject.onComplete();
         pendingSubject.onComplete();
         contactListSubject.onComplete();
-        trustRequestsSubject.onComplete();
+        //trustRequestsSubject.onComplete();
     }
 
     public Observable<List<Conversation>> getConversationsSubject() {
@@ -161,10 +210,6 @@ public class Account {
         return conversationSubject;
     }
 
-    public Observable<SmartListViewModel> getConversationViewModel() {
-        return conversationSubject.map(c -> new SmartListViewModel(accountID, c.getContact(), c.getLastEvent()));
-    }
-
     public Observable<List<Conversation>> getPendingSubject() {
         return pendingSubject;
     }
@@ -217,7 +262,7 @@ public class Account {
         }
     }
 
-    private void conversationChanged() {
+    public void conversationChanged() {
         conversationsChanged = true;
         if (historyLoaded) {
             conversationsSubject.onNext(new ArrayList<>(getSortedConversations()));
@@ -243,13 +288,15 @@ public class Account {
 
     private void updateUnreadConversations() {
         int unread = 0;
-        for (Conversation model : sortedConversations) {
-            Interaction last = model.getLastEvent();
-            if (last != null && !last.isRead())
-                unread++;
+        synchronized (sortedConversations) {
+            for (Conversation model : sortedConversations) {
+                Interaction last = model.getLastEvent();
+                if (last != null && !last.isRead())
+                    unread++;
+            }
+            // Log.w(TAG, "updateUnreadConversations " + unread);
+            unreadConversationsSubject.onNext(unread);
         }
-        // Log.w(TAG, "updateUnreadConversations " + unread);
-        unreadConversationsSubject.onNext(unread);
     }
 
     private void updateUnreadPending() {
@@ -282,13 +329,17 @@ public class Account {
     }
 
     public void updated(Conversation conversation) {
-        String key = conversation.getContact().getPrimaryUri().getUri();
+        String key = conversation.getUri().getUri();
         if (conversation == conversations.get(key))
             conversationUpdated(conversation);
         else if (conversation == pending.get(key))
             pendingUpdated(conversation);
         else if (conversation == cache.get(key)) {
+            if (isJami() && !conversation.isSwarm() && conversation.getContacts().size() == 1 && !conversation.getContact().getConversationUri().blockingFirst().equals(conversation.getUri()))  {
+                return;
+            }
             if (mContacts.containsKey(key) || !isJami()) {
+                Log.w(TAG, "updated " + conversation.getAccountId() + " contact " + key);
                 conversations.put(key, conversation);
                 conversationChanged();
             } else {
@@ -320,7 +371,7 @@ public class Account {
     }
 
     public Conversation onDataTransferEvent(DataTransfer transfer) {
-        Conversation conversation = getByUri(new Uri(transfer.getPeerId()));
+        Conversation conversation = (Conversation) transfer.getConversation();
         InteractionStatus transferEventCode = transfer.getStatus();
         if (transferEventCode == InteractionStatus.TRANSFER_CREATED) {
             conversation.addFileTransfer(transfer);
@@ -342,22 +393,27 @@ public class Account {
             CallContact contact = mContactCache.get(key);
             if (contact == null) {
                 if (isSip())
-                    contact = CallContact.buildSIP(new Uri(key));
+                    contact = CallContact.buildSIP(Uri.fromString(key));
                 else
-                    contact = CallContact.build(key);
+                    contact = CallContact.build(key, isMe(key));
                 mContactCache.put(key, contact);
             }
             return contact;
         }
     }
 
+    boolean isMe(String uri) {
+        //Log.w(TAG, "isMe " + uri + " " + getUsername());
+        return getUsername().equals(uri);
+    }
+
     public CallContact getContactFromCache(Uri uri) {
         return getContactFromCache(uri.getUri());
     }
 
     public void dispose() {
         contactListSubject.onComplete();
-        trustRequestsSubject.onComplete();
+        //trustRequestsSubject.onComplete();
     }
 
     public Map<String, String> getDevices() {
@@ -610,7 +666,7 @@ public class Account {
     public void addContact(String id, boolean confirmed) {
         CallContact callContact = mContacts.get(id);
         if (callContact == null) {
-            callContact = getContactFromCache(new Uri(id));
+            callContact = getContactFromCache(Uri.fromId(id));
             mContacts.put(id, callContact);
         }
         callContact.setAddedDate(new Date());
@@ -622,7 +678,7 @@ public class Account {
         TrustRequest req = mRequests.get(id);
         if (req != null) {
             mRequests.remove(id);
-            trustRequestsSubject.onNext(mRequests.values());
+            //trustRequestsSubject.onNext(mRequests.values());
         }
         contactAdded(callContact);
         //contactSubject.onNext(new ContactEvent(callContact, true));
@@ -633,7 +689,7 @@ public class Account {
         CallContact callContact = mContacts.get(id);
         if (banned) {
             if (callContact == null) {
-                callContact = getContactFromCache(new Uri(id));
+                callContact = getContactFromCache(Uri.fromId(id));
                 mContacts.put(id, callContact);
             }
             callContact.setStatus(CallContact.Status.BANNED);
@@ -643,10 +699,10 @@ public class Account {
         TrustRequest req = mRequests.get(id);
         if (req != null) {
             mRequests.remove(id);
-            trustRequestsSubject.onNext(mRequests.values());
+            //trustRequestsSubject.onNext(mRequests.values());
         }
         if (callContact != null) {
-            contactRemoved(callContact.getPrimaryUri());
+            contactRemoved(callContact.getUri());
             //contactSubject.onNext(new ContactEvent(callContact, false));
         }
         contactListSubject.onNext(mContacts.values());
@@ -656,7 +712,7 @@ public class Account {
         String contactId = contact.get(CONTACT_ID);
         CallContact callContact = mContacts.get(contactId);
         if (callContact == null) {
-            callContact = getContactFromCache(new Uri(contactId));
+            callContact = getContactFromCache(Uri.fromId(contactId));
         }
         String addedStr = contact.get(CONTACT_ADDED);
         if (!StringUtils.isEmpty(addedStr)) {
@@ -696,21 +752,31 @@ public class Account {
     }
 
     public TrustRequest getRequest(Uri uri) {
-        return mRequests.get(uri.getRawRingId());
+        return mRequests.get(uri.getUri());
     }
 
     public void addRequest(TrustRequest request) {
+        //Log.w(TAG, "addRequest start");
         synchronized (pending) {
-            mRequests.put(request.getContactId(), request);
-            //trustRequestSubject.onNext(new RequestEvent(request, true));
-            trustRequestsSubject.onNext(mRequests.values());
+            //boolean isSwarm = request.getConversationId() != null;
+            String key = request.getUri().getUri();
+            //if (!isSwarm) {
+            mRequests.put(key, request);
+                //trustRequestSubject.onNext(new RequestEvent(request, true));
+                //trustRequestsSubject.onNext(mRequests.values());
+            //}
 
-            String key = new Uri(request.getContactId()).getUri();
             Conversation conversation = pending.get(key);
             if (conversation == null) {
-                conversation = getByKey(key);
+                /*if (isSwarm) {
+                    Log.w(TAG, "new public swarm request");
+                }*/
+                conversation = /*isSwarm ? newSwarm(key, Conversation.Mode.Public) : */getByKey(key);
                 pending.put(key, conversation);
-                conversation.addRequestEvent(request);
+                if (!conversation.isSwarm()) {
+                    CallContact contact = getContactFromCache(request.getUri());
+                    conversation.addRequestEvent(request, contact);
+                }
                 pendingChanged();
             }
         }
@@ -720,17 +786,18 @@ public class Account {
         Log.w(TAG, "setRequests " + requests.size());
         synchronized (pending) {
             for (TrustRequest request : requests) {
-                mRequests.put(request.getContactId(), request);
-                String key = new Uri(request.getContactId()).getUri();
+                String key = request.getUri().getUri();
+                mRequests.put(key, request);
                 Conversation conversation = pending.get(key);
                 if (conversation == null) {
                     conversation = getByKey(key);
                     pending.put(key, conversation);
-                    conversation.addRequestEvent(request);
+                    CallContact contact = getContactFromCache(request.getUri());
+                    conversation.addRequestEvent(request, contact);
                 }
                 //trustRequestSubject.onNext(new RequestEvent(request, true));
             }
-            trustRequestsSubject.onNext(mRequests.values());
+            //trustRequestsSubject.onNext(mRequests.values());
             pendingChanged();
         }
     }
@@ -741,7 +808,7 @@ public class Account {
             TrustRequest request = mRequests.remove(contactUri);
             if (request != null) {
                 //trustRequestSubject.onNext(new RequestEvent(request, true));
-                trustRequestsSubject.onNext(mRequests.values());
+                //trustRequestsSubject.onNext(mRequests.values());
             }
             if (pending.remove(contactUri) != null) {
                 pendingChanged();
@@ -751,11 +818,11 @@ public class Account {
         return false;
     }
 
-    public void registeredNameFound(int state, String address, String name) {
-        Uri uri = new Uri(address);
-        CallContact contact = getContactFromCache(uri);
+    public boolean registeredNameFound(int state, String address, String name) {
+        Uri uri = Uri.fromString(address);
+        String key = uri.getUri();
+        CallContact contact = getContactFromCache(key);
         if (contact.setUsername(state == 0 ? name : null)) {
-            String key = uri.getUri();
             synchronized (conversations) {
                 Conversation conversation = conversations.get(key);
                 if (conversation != null)
@@ -765,18 +832,22 @@ public class Account {
                 if (pending.containsKey(key))
                     pendingRefreshed();
             }
+            return true;
         }
+        return false;
     }
 
     public Conversation getByUri(Uri uri) {
-        if (uri != null && !uri.isEmpty()) {
-            return getByKey(uri.getUri());
-        }
-        return null;
+        //Log.w(TAG, "getByUri " + getAccountID() + " " + uri);
+        if (uri == null || uri.isEmpty())
+            return null;
+        return uri.isSwarm()
+                ? getSwarm(uri.getRawRingId())
+                : getByKey(uri.getUri());
     }
 
     public Conversation getByUri(String uri) {
-        return getByUri(new Uri(uri));
+        return getByUri(Uri.fromString(uri));
     }
 
     private Conversation getByKey(String key) {
@@ -786,6 +857,7 @@ public class Account {
         }
         CallContact contact = getContactFromCache(key);
         conversation = new Conversation(getAccountID(), contact);
+        //Log.w(TAG, "getByKey " + getAccountID() + " contact " + key);
         cache.put(key, conversation);
         return conversation;
     }
@@ -793,16 +865,19 @@ public class Account {
     public void setHistoryLoaded(List<Conversation> conversations) {
         if (historyLoaded)
             return;
-        Log.w(TAG, "setHistoryLoaded() " + conversations.size());
-        for (Conversation c : conversations)
-            updated(c);
+        //Log.w(TAG, "setHistoryLoaded " + getAccountID() + " " + conversations.size());
+        for (Conversation c : conversations) {
+            CallContact contact = c.getContact();
+            if (!c.isSwarm() && contact != null && contact.getConversationUri().blockingFirst().equals(c.getUri()))
+                updated(c);
+        }
         historyLoaded = true;
         conversationChanged();
         pendingChanged();
     }
 
     private List<Conversation> getSortedConversations() {
-        Log.w(TAG, "getSortedConversations() " + Thread.currentThread().getId());
+        //Log.w(TAG, "getSortedConversations() " + Thread.currentThread().getId());
         synchronized (sortedConversations) {
             if (conversationsChanged) {
                 sortedConversations.clear();
@@ -836,10 +911,15 @@ public class Account {
 
 
     private void contactAdded(CallContact contact) {
-        Uri uri = contact.getPrimaryUri();
+        Uri uri = contact.getUri();
         String key = uri.getUri();
+        //Log.w(TAG, "contactAdded " + getAccountID() + " " + key + " " + contact.getConversationUri());
+        if (!contact.getConversationUri().blockingFirst().equals(contact.getUri())) {
+            // Don't add conversation if we have a swarm conversation
+            return;
+        }
         synchronized (conversations) {
-            if (conversations.get(key) != null)
+            if (conversations.containsKey(key))
                 return;
             synchronized (pending) {
                 Conversation pendingConversation = pending.get(key);
@@ -851,7 +931,7 @@ public class Account {
                     conversations.put(key, pendingConversation);
                     pendingChanged();
                 }
-                pendingConversation.addContactEvent();
+                pendingConversation.addContactEvent(contact);
             }
             conversationChanged();
         }
@@ -880,6 +960,7 @@ public class Account {
     }
 
     public void presenceUpdate(String contactUri, boolean isOnline) {
+        Log.w(TAG, "presenceUpdate " + contactUri + " " + isOnline);
         CallContact contact = getContactFromCache(contactUri);
         if (contact.isOnline() == isOnline)
             return;
@@ -896,10 +977,15 @@ public class Account {
         }
     }
 
-    public void composingStatusChanged(Uri contactUri, ComposingStatus status) {
-        Conversation conversation = getByUri(contactUri);
-        if (conversation != null)
-            conversation.composingStatusChanged(getContactFromCache(contactUri), status);
+    public void composingStatusChanged(String conversationId, Uri contactUri, ComposingStatus status) {
+        boolean isSwarm = !StringUtils.isEmpty(conversationId);
+        Conversation conversation = isSwarm ? getSwarm(conversationId) : getByUri(contactUri);
+        if (conversation != null) {
+            CallContact contact = isSwarm ? conversation.findContact(contactUri) : getContactFromCache(contactUri);
+            if (contact != null) {
+                conversation.composingStatusChanged(contact, status);
+            }
+        }
     }
 
     synchronized public long onLocationUpdate(AccountService.Location location) {
@@ -980,6 +1066,8 @@ public class Account {
     public Observable<Observable<ContactLocation>> getLocationUpdates(Uri contactId) {
         CallContact contact = getContactFromCache(contactId);
         Log.w(TAG, "getLocationUpdates " + contactId + " " + contact);
+        if (contact == null || contact.isUser())
+            return Observable.empty();
         return mLocationSubject
                 .flatMapMaybe(locations -> {
                     Observable<ContactLocation> r = locations.get(contact);
@@ -1029,6 +1117,14 @@ public class Account {
         mLoadedProfile = profile;
     }
 
+    public DataTransfer getDataTransfer(long id) {
+        return mDataTransfers.get(id);
+    }
+
+    public void putDataTransfer(long transferId, DataTransfer transfer) {
+        mDataTransfers.put(transferId, transfer);
+    }
+
     private static class ConversationComparator implements Comparator<Conversation> {
         @Override
         public int compare(Conversation a, Conversation b) {
diff --git a/ring-android/libringclient/src/main/java/cx/ring/model/CallContact.java b/ring-android/libringclient/src/main/java/cx/ring/model/CallContact.java
index c415044a02e9231d37456753f5412b3f2a207b38..03cfb81a8edbc2eb895b21d1d902e946b1e077ed 100644
--- a/ring-android/libringclient/src/main/java/cx/ring/model/CallContact.java
+++ b/ring-android/libringclient/src/main/java/cx/ring/model/CallContact.java
@@ -38,11 +38,11 @@ public class CallContact {
 
     public enum Status {BANNED, REQUEST_SENT, CONFIRMED, NO_REQUEST}
 
-    private long mId;
-    private String mKey;
+    private final Uri mUri;
+
     private String mUsername = null;
     private long mPhotoId;
-    private final ArrayList<Phone> mPhones;
+    private final ArrayList<Phone> mPhones = new ArrayList<>();
     private final boolean isUser;
     private boolean stared = false;
     private boolean isFromSystem = false;
@@ -50,8 +50,13 @@ public class CallContact {
     private Date mAddedDate = null;
     private boolean mOnline = false;
 
+    private long mId;
+    private String mLookupKey;
+
     private boolean usernameLoaded = false;
     public boolean detailsLoaded = false;
+    //private Uri mConversationUri = null;
+    private final BehaviorSubject<Uri> mConversationUri;
 
     // Profile
     private String mDisplayName;
@@ -63,38 +68,44 @@ public class CallContact {
     private Observable<Boolean> mContactPresenceObservable;
     private Emitter<Boolean> mContactPresenceEmitter;
 
-    public CallContact(long cID) {
-        this(cID, null, null, UNKNOWN_ID);
+    public CallContact(Uri uri) {
+        this(uri, false);
     }
 
-    public CallContact(long cID, String k, String displayName, long photoID) {
-        this(cID, k, displayName, photoID, new ArrayList<>(), false);
+    public CallContact(Uri uri, boolean user) {
+        this(uri, null, user);
     }
 
-    private CallContact(long cID, String k, String displayName, long photoID, ArrayList<Phone> p, boolean user) {
-        mId = cID;
-        mKey = k;
+    private CallContact(Uri uri, String displayName, boolean user) {
+        mUri = uri;
         mDisplayName = displayName;
-        mPhones = p;
-        mPhotoId = photoID;
         isUser = user;
-        if (cID != UNKNOWN_ID && (displayName == null || !displayName.contains(PREFIX_RING))) {
+        mConversationUri = BehaviorSubject.createDefault(mUri);
+        /*if (cID != UNKNOWN_ID && (displayName == null || !displayName.contains(PREFIX_RING))) {
             mStatus = Status.CONFIRMED;
-        }
+        }*/
+    }
+
+    public void setConversationUri(Uri conversationUri) {
+        mConversationUri.onNext(conversationUri);
+        //mConversationUri = conversationUri;
+    }
+
+    public Observable<Uri> getConversationUri() {
+        return mConversationUri;
     }
 
     public static CallContact buildSIP(Uri to) {
-        ArrayList<Phone> phones = new ArrayList<>();
-        phones.add(new Phone(to, 0));
-        CallContact contact = new CallContact(UNKNOWN_ID, null, null, 0, phones, false);
+        CallContact contact = new CallContact(to);
         contact.usernameLoaded = true;
         return contact;
     }
 
-    public static CallContact build(String to) {
-        ArrayList<Phone> phones = new ArrayList<>();
-        phones.add(new Phone(to, 0));
-        return new CallContact(UNKNOWN_ID, null, null, 0, phones, false);
+    public static CallContact build(String uri, boolean isUser) {
+        return new CallContact(Uri.fromString(uri), isUser);
+    }
+    public static CallContact build(String uri) {
+        return build(uri, false);
     }
 
     public Observable<CallContact> getUpdatesSubject() {
@@ -137,8 +148,13 @@ public class CallContact {
         mContactUpdates.onNext(this);
     }
 
-    public void setContactInfos(String k, String displayName, long photo_id) {
-        mKey = k;
+    public void setSystemId(long id) {
+        mId = id;
+    }
+
+    public void setSystemContactInfo(long id, String k, String displayName, long photo_id) {
+        mId = id;
+        mLookupKey = k;
         mDisplayName = displayName;
         this.mPhotoId = photo_id;
         if (mUsername == null && displayName.contains(PREFIX_RING)) {
@@ -149,7 +165,7 @@ public class CallContact {
     public static String canonicalNumber(String number) {
         if (number == null || number.isEmpty())
             return null;
-        return new Uri(number).getRawUriString();
+        return Uri.fromString(number).getRawUriString();
     }
 
     public ArrayList<String> getIds() {
@@ -192,7 +208,7 @@ public class CallContact {
     }
 
     public boolean hasNumber(String number) {
-        return hasNumber(new Uri(number));
+        return hasNumber(Uri.fromString(number));
     }
 
     public boolean hasNumber(Uri number) {
@@ -213,15 +229,15 @@ public class CallContact {
         this.mId = id;
     }
 
-    public String getKey() {
+    /*public String getKey() {
         return mKey;
-    }
+    }*/
 
     public String getPrimaryNumber() {
-        return getPrimaryUri().getRawRingId();
+        return getUri().getRawRingId();
     }
-    public Uri getPrimaryUri() {
-        return getPhones().get(0).getNumber();
+    public Uri getUri() {
+        return mUri;
     }
 
     public void setStared() {
@@ -304,8 +320,8 @@ public class CallContact {
     public String getRingUsername() {
         if (!StringUtils.isEmpty(mUsername)) {
             return mUsername;
-        } else if (usernameLoaded && !mPhones.isEmpty()) {
-            return getPrimaryUri().getRawUriString();
+        } else if (usernameLoaded) {
+            return getUri().getRawUriString();
         } else {
             return "";
         }
diff --git a/ring-android/libringclient/src/main/java/cx/ring/model/Conference.java b/ring-android/libringclient/src/main/java/cx/ring/model/Conference.java
index 8c8aa5b7c7f006c0419d63b7e83a45bfafa040de..2e6f3551042c47d25e34022d1dd412bb86bc6ea7 100644
--- a/ring-android/libringclient/src/main/java/cx/ring/model/Conference.java
+++ b/ring-android/libringclient/src/main/java/cx/ring/model/Conference.java
@@ -55,7 +55,7 @@ public class Conference {
     private final Set<CallContact> mParticipantRecordingSet = new HashSet<>();
     private final Subject<Set<CallContact>> mParticipantRecording = BehaviorSubject.createDefault(Collections.emptySet());
 
-    private String mId;
+    private final String mId;
     private SipCall.CallStatus mConfState;
     private final ArrayList<SipCall> mParticipants;
     private boolean mRecording;
@@ -172,7 +172,7 @@ public class Conference {
 
     public SipCall findCallByContact(Uri uri) {
         for (SipCall call : mParticipants) {
-            if (call.getContact().getPrimaryUri().toString().equals(uri.toString()))
+            if (call.getContact().getUri().toString().equals(uri.toString()))
                 return call;
         }
         return null;
diff --git a/ring-android/libringclient/src/main/java/cx/ring/model/ContactEvent.java b/ring-android/libringclient/src/main/java/cx/ring/model/ContactEvent.java
index 895951c306f4e91b8f11ea08ecac1d505a8cec4b..871ed26bf00917d5cc05b5374cb2a96fb9b98a4e 100644
--- a/ring-android/libringclient/src/main/java/cx/ring/model/ContactEvent.java
+++ b/ring-android/libringclient/src/main/java/cx/ring/model/ContactEvent.java
@@ -49,7 +49,7 @@ public class ContactEvent extends Interaction {
 
     public ContactEvent(CallContact contact) {
         mContact = contact;
-        mAuthor = contact.getPrimaryUri().getUri();
+        mAuthor = contact.getUri().getUri();
         mType = InteractionType.CONTACT.toString();
         event = Event.ADDED;
         mStatus = InteractionStatus.SUCCESS.toString();
@@ -60,7 +60,7 @@ public class ContactEvent extends Interaction {
     public ContactEvent(CallContact contact, TrustRequest request) {
         this.request = request;
         mContact = contact;
-        mAuthor = contact.getPrimaryUri().getUri();
+        mAuthor = contact.getUri().getUri();
         mTimestamp = request.getTimestamp();
         mType = InteractionType.CONTACT.toString();
         event = Event.INCOMING_REQUEST;
diff --git a/ring-android/libringclient/src/main/java/cx/ring/model/Conversation.java b/ring-android/libringclient/src/main/java/cx/ring/model/Conversation.java
index 1763e563d228049ff310ab83b987fc784a3a40f4..1c37a4145f841363cfbdc4ca3d565563a2a33421 100644
--- a/ring-android/libringclient/src/main/java/cx/ring/model/Conversation.java
+++ b/ring-android/libringclient/src/main/java/cx/ring/model/Conversation.java
@@ -23,14 +23,18 @@ package cx.ring.model;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
 import java.util.NavigableMap;
+import java.util.Set;
 import java.util.TreeMap;
 
 import cx.ring.model.Interaction.InteractionType;
 import cx.ring.utils.Log;
+import cx.ring.utils.StringUtils;
 import cx.ring.utils.Tuple;
 import io.reactivex.Observable;
 import io.reactivex.Single;
@@ -39,8 +43,6 @@ import io.reactivex.subjects.PublishSubject;
 import io.reactivex.subjects.Subject;
 
 public class Conversation extends ConversationHistory {
-
-
     private static final String TAG = Conversation.class.getSimpleName();
 
     private final String mAccountId;
@@ -58,9 +60,16 @@ public class Conversation extends ConversationHistory {
     private final Subject<List<Conference>> callsSubject = BehaviorSubject.create();
     private final Subject<Account.ComposingStatus> composingStatusSubject = BehaviorSubject.createDefault(Account.ComposingStatus.Idle);
     private final Subject<Integer> color = BehaviorSubject.create();
+    private final Subject<List<CallContact>> mContactSubject = BehaviorSubject.create();
 
     private Single<Conversation> isLoaded = 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;
+
     // runtime flag set to true if the user is currently viewing this conversation
     private boolean mVisible = false;
 
@@ -70,8 +79,17 @@ public class Conversation extends ConversationHistory {
     public Conversation(String accountId, CallContact contact) {
         mAccountId = accountId;
         mContacts = Collections.singletonList(contact);
-        mKey = contact.getPrimaryUri();
-        mParticipant = contact.getPrimaryUri().getUri();
+        mKey = contact.getUri();
+        mParticipant = contact.getUri().getUri();
+        mContactSubject.onNext(mContacts);
+        mMode = null;
+    }
+
+    public Conversation(String accountId, Uri uri, Mode mode) {
+        mAccountId = accountId;
+        mKey = uri;
+        mContacts = new ArrayList<>(3);
+        mMode = mode;
     }
 
     public Conference getConference(String id) {
@@ -90,12 +108,32 @@ public class Conversation extends ConversationHistory {
         return mKey;
     }
 
-    public CharSequence getDisplayName() {
+    public Mode getMode() { return mMode; }
+
+    public boolean isSwarm() {
+        return Uri.SWARM_SCHEME.equals(getUri().getScheme());
+    }
+
+    public boolean matches(String query) {
+        for (CallContact contact : getContacts()) {
+            if (contact.matches(query))
+                return true;
+        }
+        return false;
+    }
+
+    public String getDisplayName() {
         return mContacts.get(0).getDisplayName();
     }
 
     public void addContact(CallContact contact) {
         mContacts.add(contact);
+        mContactSubject.onNext(mContacts);
+    }
+
+    public void removeContact(CallContact contact)  {
+        mContacts.remove(contact);
+        mContactSubject.onNext(mContacts);
     }
 
     public String getTitle() {
@@ -105,33 +143,76 @@ public class Conversation extends ConversationHistory {
             return mContacts.get(0).getDisplayName();
         }
         StringBuilder ret = new StringBuilder();
-        Iterator<CallContact> it = mContacts.iterator();
-        while (it.hasNext()) {
-            CallContact c = it.next();
-            ret.append(c.getDisplayName());
-            if (it.hasNext())
-                ret.append(", ");
+        ArrayList<String> names = new ArrayList<>(mContacts.size());
+        int target = mContacts.size();
+        for (CallContact c : mContacts) {
+            if (c.isUser()) {
+                target--;
+                continue;
+            }
+            String displayName = c.getDisplayName();
+            if (!StringUtils.isEmpty(displayName)) {
+                names.add(displayName);
+                if (names.size() == 3)
+                    break;
+            }
         }
-        return ret.toString();
+        ret.append(StringUtils.join(", ", names));
+        if (!names.isEmpty() && names.size() < target) {
+            ret.append(" + ").append(mContacts.size() - names.size());
+        }
+        String result = ret.toString();
+        return result.isEmpty() ? mKey.getRawUriString() : result;
     }
 
     public String getUriTitle() {
         if (mContacts.isEmpty()) {
             return null;
         } else if (mContacts.size() == 1) {
-            return mContacts.get(0).getDisplayName();
+            return mContacts.get(0).getRingUsername();
         }
         StringBuilder ret = new StringBuilder();
         Iterator<CallContact> it = mContacts.iterator();
         while (it.hasNext()) {
             CallContact c = it.next();
-            ret.append(c.getDisplayName());
+            if (c.isUser())
+                continue;
+            ret.append(c.getRingUsername());
             if (it.hasNext())
                 ret.append(", ");
         }
         return ret.toString();
     }
 
+    public Observable<List<CallContact>> getContactUpdates() {
+        return mContactSubject;
+    }
+
+    public String readMessages() {
+        Interaction interaction = null;
+        for (String branch : mBranches) {
+            Interaction i = mMessages.get(branch);
+            if (i != null && !i.isRead()) {
+                i.read();
+                interaction = i;
+                lastRead = i.getMessageId();
+            }
+        }
+        return interaction == null ? null : interaction.getMessageId();
+    }
+
+    public Interaction getMessage(String messageId) {
+        return mMessages.get(messageId);
+    }
+
+    public void setLastMessageRead(String lastMessageRead) {
+        lastRead = lastMessageRead;
+    }
+
+    public String getLastRead() {
+        return lastRead;
+    }
+
     public enum ElementStatus {
         UPDATE, REMOVE, ADD
     }
@@ -199,13 +280,22 @@ public class Conversation extends ConversationHistory {
         return mContacts;
     }
 
-    @Deprecated
     public CallContact getContact() {
-        return mContacts.get(0);
+        if (mContacts.size() == 1)
+            return mContacts.get(0);
+        if (isSwarm()) {
+            if (mContacts.size() > 2)
+                throw new IllegalStateException("getContact() called for group conversation of size " + mContacts.size());
+        }
+        for (CallContact contact : mContacts) {
+            if (!contact.isUser())
+                return contact;
+        }
+        return null;
     }
 
     public void addCall(SipCall call) {
-        if (getCallHistory().contains(call)) {
+        if (!isSwarm() && getCallHistory().contains(call)) {
             return;
         }
         mDirty = true;
@@ -213,6 +303,25 @@ public class Conversation extends ConversationHistory {
         updatedElementSubject.onNext(new Tuple<>(call, ElementStatus.ADD));
     }
 
+    private void setInteractionProperties(Interaction interaction) {
+        interaction.setAccount(getAccountId());
+        if (interaction.getContact() == null) {
+            if (mContacts.size() == 1)
+                interaction.setContact(mContacts.get(0));
+            else
+                interaction.setContact(findContact(Uri.fromString(interaction.getAuthor())));
+        }
+    }
+
+    public CallContact findContact(Uri uri) {
+        for (CallContact contact : mContacts)  {
+            if (contact.getUri().equals(uri)) {
+                return contact;
+            }
+        }
+        return null;
+    }
+
     public void addTextMessage(TextMessage txt) {
         if (mVisible) {
             txt.read();
@@ -220,24 +329,24 @@ public class Conversation extends ConversationHistory {
         if (txt.getConversation() == null) {
             Log.e(TAG, "Error in conversation class... No conversation is attached to this interaction");
         }
-        if (txt.getContact() == null) {
-            txt.setContact(getContact());
-        }
+        setInteractionProperties(txt);
         mHistory.put(txt.getTimestamp(), txt);
         mDirty = true;
         mAggregateHistory.add(txt);
         updatedElementSubject.onNext(new Tuple<>(txt, ElementStatus.ADD));
     }
 
-    public void addRequestEvent(TrustRequest request) {
-        ContactEvent event = new ContactEvent(getContact(), request);
+    public void addRequestEvent(TrustRequest request, CallContact contact) {
+        if (isSwarm())
+            return;
+        ContactEvent event = new ContactEvent(contact, request);
         mDirty = true;
         mAggregateHistory.add(event);
         updatedElementSubject.onNext(new Tuple<>(event, ElementStatus.ADD));
     }
 
-    public void addContactEvent() {
-        ContactEvent event = new ContactEvent(getContact());
+    public void addContactEvent(CallContact contact) {
+        ContactEvent event = new ContactEvent(contact);
         mDirty = true;
         mAggregateHistory.add(event);
         updatedElementSubject.onNext(new Tuple<>(event, ElementStatus.ADD));
@@ -259,11 +368,9 @@ public class Conversation extends ConversationHistory {
     }
 
     public void updateTextMessage(TextMessage text) {
-        text.setContact(getContact());
-        long time = text.getTimestamp();
-        NavigableMap<Long, Interaction> msgs = mHistory.subMap(time, true, time, true);
-        for (Interaction txt : msgs.values()) {
-            if (txt.getId() == text.getId()) {
+        if (isSwarm()) {
+            TextMessage txt = (TextMessage) mMessages.get(text.getMessageId());
+            if (txt != null) {
                 txt.setStatus(text.getStatus());
                 updatedElementSubject.onNext(new Tuple<>(txt, ElementStatus.UPDATE));
                 if (text.getStatus() == Interaction.InteractionStatus.DISPLAYED) {
@@ -272,10 +379,28 @@ public class Conversation extends ConversationHistory {
                         lastDisplayedSubject.onNext(text);
                     }
                 }
-                return;
+            } else {
+                Log.e(TAG, "Can't find swarm message to update: " + text.getMessageId());
             }
+        } else {
+            setInteractionProperties(text);
+            long time = text.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 Tuple<>(txt, ElementStatus.UPDATE));
+                    if (text.getStatus() == Interaction.InteractionStatus.DISPLAYED) {
+                        if (lastDisplayed == null || lastDisplayed.getTimestamp() < text.getTimestamp()) {
+                            lastDisplayed = text;
+                            lastDisplayedSubject.onNext(text);
+                        }
+                    }
+                    return;
+                }
+            }
+            Log.e(TAG, "Can't find message to update: " + text.getId());
         }
-        Log.e(TAG, "Can't find message to update: " + text.getId());
     }
 
     public ArrayList<Interaction> getAggregateHistory() {
@@ -289,6 +414,7 @@ public class Conversation extends ConversationHistory {
 
     public void sortHistory() {
         if (mDirty) {
+            Log.w(TAG, "sortHistory()");
             synchronized (mAggregateHistory) {
                 Collections.sort(mAggregateHistory, (c1, c2) -> Long.compare(c1.getTimestamp(), c2.getTimestamp()));
             }
@@ -330,13 +456,23 @@ public class Conversation extends ConversationHistory {
 
     public TreeMap<Long, TextMessage> getUnreadTextMessages() {
         TreeMap<Long, TextMessage> texts = new TreeMap<>();
-        for (Map.Entry<Long, Interaction> entry : mHistory.descendingMap().entrySet()) {
-            Interaction value = entry.getValue();
-            if (value.getType() == InteractionType.TEXT) {
-                TextMessage message = (TextMessage) value;
-                if (message.isRead())
+        if (isSwarm()) {
+            for(int j = mAggregateHistory.size() - 1; j >= 0; j--) {
+                Interaction i = mAggregateHistory.get(j);
+                if (i.isRead())
                     break;
-                texts.put(entry.getKey(), message);
+                if (i instanceof TextMessage)
+                    texts.put(i.getTimestamp(), (TextMessage) i);
+            }
+        } else {
+            for (Map.Entry<Long, Interaction> entry : mHistory.descendingMap().entrySet()) {
+                Interaction value = entry.getValue();
+                if (value.getType() == InteractionType.TEXT) {
+                    TextMessage message = (TextMessage) value;
+                    if (message.isRead())
+                        break;
+                    texts.put(entry.getKey(), message);
+                }
             }
         }
         return texts;
@@ -380,8 +516,8 @@ public class Conversation extends ConversationHistory {
         mAggregateHistory.clear();
         mHistory.clear();
         mDirty = false;
-        if(!delete)
-            mAggregateHistory.add(new ContactEvent(getContact()));
+        if (!delete && mContacts.size() == 1)
+            mAggregateHistory.add(new ContactEvent(mContacts.get(0)));
         clearedSubject.onNext(mAggregateHistory);
     }
 
@@ -404,8 +540,7 @@ public class Conversation extends ConversationHistory {
         Interaction last = null;
         for (Interaction i : loadedConversation) {
             Interaction interaction = getTypedInteraction(i);
-            interaction.setAccount(mAccountId);
-            interaction.setContact(getContact());
+            setInteractionProperties(interaction);
             mAggregateHistory.add(interaction);
             mHistory.put(interaction.getTimestamp(), interaction);
             if (!i.isIncoming() && i.getStatus() == Interaction.InteractionStatus.DISPLAYED)
@@ -419,8 +554,7 @@ public class Conversation extends ConversationHistory {
     }
 
     public void addElement(Interaction interaction) {
-        interaction.setAccount(mAccountId);
-        interaction.setContact(getContact());
+        setInteractionProperties(interaction);
         if (interaction.getType() == InteractionType.TEXT) {
             TextMessage msg = new TextMessage(interaction);
             addTextMessage(msg);
@@ -436,6 +570,59 @@ 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);
+        }
+        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;
+        }
+        if (mAggregateHistory.isEmpty() || interaction.getParentIds().contains(mAggregateHistory.get(mAggregateHistory.size()-1).getMessageId())) {
+            // New leaf
+            mAggregateHistory.add(interaction);
+            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())) {
+                    mAggregateHistory.add(i, interaction);
+                    updatedElementSubject.onNext(new Tuple<>(interaction, ElementStatus.ADD));
+                    break;
+                }
+            }
+        }
+        return newMessage;
+    }
+
+    public Collection<String> getSwarmRoot() {
+        return mRoots;
+    }
+
     public void updateFileTransfer(DataTransfer transfer, Interaction.InteractionStatus eventCode) {
         DataTransfer dataTransfer = (DataTransfer) findConversationElement(transfer.getId());
         if (dataTransfer != null) {
@@ -469,11 +656,18 @@ public class Conversation extends ConversationHistory {
         return mAccountId;
     }
 
+    public enum Mode {
+        OneToOne,
+        AdminInvitesOnly,
+        InvitesOnly,
+        Public
+    }
+
     public interface ConversationActionCallback {
 
-        void removeConversation(CallContact callContact);
+        void removeConversation(Uri callContact);
 
-        void clearConversation(CallContact callContact);
+        void clearConversation(Uri callContact);
 
         void copyContactNumberToClipboard(String contactNumber);
 
diff --git a/ring-android/libringclient/src/main/java/cx/ring/model/ConversationHistory.java b/ring-android/libringclient/src/main/java/cx/ring/model/ConversationHistory.java
index 832bb6c3b37081b9efbbe1f10da01e9b6e1bd838..38cc05d1de3fb89beafd8fa8629808763805fb4a 100644
--- a/ring-android/libringclient/src/main/java/cx/ring/model/ConversationHistory.java
+++ b/ring-android/libringclient/src/main/java/cx/ring/model/ConversationHistory.java
@@ -37,9 +37,6 @@ public class ConversationHistory {
     @DatabaseField(columnName = COLUMN_EXTRA_DATA)
     String mExtraData;
 
-    String account;
-
-
     /* Needed by ORMLite */
     public ConversationHistory() {
     }
@@ -70,12 +67,4 @@ public class ConversationHistory {
         return mParticipant;
     }
 
-    public void setAccount(String account) {
-        this.account = account;
-    }
-
-    public String getAccount() {
-        return account;
-    }
-
 }
diff --git a/ring-android/libringclient/src/main/java/cx/ring/model/DataTransfer.java b/ring-android/libringclient/src/main/java/cx/ring/model/DataTransfer.java
index e809d3916e236f1b2e69f2cdaa70631824c8bb53..146f58549dc17a10d41d331bdbfe3f240bee06b5 100644
--- a/ring-android/libringclient/src/main/java/cx/ring/model/DataTransfer.java
+++ b/ring-android/libringclient/src/main/java/cx/ring/model/DataTransfer.java
@@ -20,6 +20,7 @@
  */
 package cx.ring.model;
 
+import java.io.File;
 import java.util.Set;
 
 import cx.ring.utils.HashUtils;
@@ -29,8 +30,10 @@ public class DataTransfer extends Interaction {
 
     private long mTotalSize;
     private long mBytesProgress;
-    private String mPeerId;
+    //private final String mPeerId;
     private String mExtension;
+    //private String mTransferId;
+    public File destination;
 
     private static final Set<String> IMAGE_EXTENSIONS = HashUtils.asSet("jpg", "jpeg", "png", "gif");
     private static final Set<String> AUDIO_EXTENSIONS = HashUtils.asSet("ogg", "mp3", "aac", "flac", "m4a");
@@ -38,11 +41,11 @@ public class DataTransfer extends Interaction {
     private static final int MAX_SIZE = 32 * 1024 * 1024;
     private static final int UNLIMITED_SIZE = 256 * 1024 * 1024;
 
-    public DataTransfer(ConversationHistory conversation, String account, String displayName, boolean isOutgoing, long totalSize, long bytesProgress, long daemonId) {
-        mAuthor = isOutgoing ? null : conversation.getParticipant();
+    public DataTransfer(ConversationHistory conversation, String peer, String account, String displayName, boolean isOutgoing, long totalSize, long bytesProgress, long daemonId) {
+        mAuthor = isOutgoing ? null : peer;
         mAccount = account;
         mConversation = conversation;
-        mPeerId = conversation.getParticipant();
+        mAuthor = peer;
         mTotalSize = totalSize;
         mBytesProgress = bytesProgress;
         mBody = displayName;
@@ -51,16 +54,15 @@ public class DataTransfer extends Interaction {
         mTimestamp = System.currentTimeMillis();
         mIsRead = 1;
         mDaemonId = daemonId;
-        mIsIncoming = mAuthor != null;
+        mIsIncoming = !isOutgoing;
     }
 
-
     public DataTransfer(Interaction interaction) {
         mId = interaction.getId();
         mDaemonId = interaction.getDaemonId();
         mAuthor = interaction.getAuthor();
         mConversation = interaction.getConversation();
-        mPeerId = interaction.getConversation().getParticipant();
+        // mPeerId = interaction.getConversation().getParticipant();
         mBody = interaction.getBody();
         mStatus = interaction.getStatus().toString();
         mType = interaction.getType().toString();
@@ -68,12 +70,29 @@ public class DataTransfer extends Interaction {
         mAccount = interaction.getAccount();
         mContact = interaction.getContact();
         mIsRead = 1;
-        mIsIncoming = mAuthor != null;
+        mIsIncoming = interaction.mIsIncoming;//mAuthor != null;
+    }
+
+    public DataTransfer(long transferId, String accountId, String peerUri, String displayName, boolean isOutgoing, long timestamp, long totalSize, long bytesProgress) {
+        mDaemonId = transferId;
+        mAccount = accountId;
+        //mTransferId = transferId;
+        //mPeerId = peerUri;
+        mBody = displayName;
+        mAuthor = peerUri;
+        mIsIncoming = !isOutgoing;
+        mTotalSize = totalSize;
+        mBytesProgress = bytesProgress;
+        mTimestamp = timestamp;
+        //mDaemonId = Long.parseUnsignedLong(transferId);
+        mType = InteractionType.DATA_TRANSFER.toString();
     }
 
     public String getExtension() {
+        if (mBody == null)
+            return null;
         if (mExtension == null)
-            mExtension = StringUtils.getFileExtension(getDisplayName()).toLowerCase();
+            mExtension = StringUtils.getFileExtension(mBody).toLowerCase();
         return mExtension;
     }
 
@@ -96,6 +115,9 @@ public class DataTransfer extends Interaction {
     }
 
     public String getStoragePath() {
+        if (StringUtils.isEmpty(mBody)) {
+            return getMessageId();
+        }
         String ext = StringUtils.getFileExtension(mBody);
         if (ext.length() > 8)
             ext = ext.substring(0, 8);
@@ -125,11 +147,6 @@ public class DataTransfer extends Interaction {
     public void setBytesProgress(long bytesProgress) { mBytesProgress = bytesProgress;
     }
 
-    public String getPeerId() {
-        return mPeerId;
-    }
-
-
     public boolean isError() {
         return getStatus().isError();
     }
diff --git a/ring-android/libringclient/src/main/java/cx/ring/model/Interaction.java b/ring-android/libringclient/src/main/java/cx/ring/model/Interaction.java
index eb13f90e0f97a44d6ca354e691a66ac9398afe17..8bbe3ddf4848f53a7a7d892f70751c1edfa27b17 100644
--- a/ring-android/libringclient/src/main/java/cx/ring/model/Interaction.java
+++ b/ring-android/libringclient/src/main/java/cx/ring/model/Interaction.java
@@ -24,6 +24,8 @@ import com.google.gson.JsonParser;
 import com.j256.ormlite.field.DatabaseField;
 import com.j256.ormlite.table.DatabaseTable;
 
+import java.util.List;
+
 @DatabaseTable(tableName = Interaction.TABLE_NAME)
 public class Interaction {
 
@@ -63,6 +65,11 @@ public class Interaction {
     @DatabaseField(columnName = COLUMN_EXTRA_FLAG)
     String mExtraFlag = new JsonObject().toString();
 
+    // Swarm
+    private String mConversationId;
+    private String mMessageId;
+    private List<String> mParentIds;
+
     /* Needed by ORMLite */
     public Interaction() {
     }
@@ -168,6 +175,18 @@ public class Interaction {
         return mDaemonId == null ? null : Long.toString(mDaemonId);
     }
 
+    public String getMessageId() {
+        return mMessageId;
+    }
+
+    public String getConversationId() {
+        return mConversationId;
+    }
+
+    public List<String> getParentIds() {
+        return mParentIds;
+    }
+
     public boolean isIncoming() {
         return mIsIncoming;
     }
@@ -184,6 +203,17 @@ public class Interaction {
         mContact = contact;
     }
 
+    public void setSwarmInfo(String conversationId) {
+        mConversationId = conversationId;
+        mMessageId = null;
+        mParentIds = null;
+    }
+    public void setSwarmInfo(String conversationId, String messageId, List<String> parents) {
+        mConversationId = conversationId;
+        mMessageId = messageId;
+        mParentIds = parents;
+    }
+
     public enum InteractionStatus {
         UNKNOWN, SENDING, SUCCESS, DISPLAYED, INVALID, FAILURE,
 
@@ -222,6 +252,7 @@ public class Interaction {
                 case 1:
                     return TRANSFER_CREATED;
                 case 2:
+                case 9:
                     return TRANSFER_ERROR;
                 case 3:
                     return TRANSFER_AWAITING_PEER;
@@ -232,11 +263,7 @@ public class Interaction {
                 case 6:
                     return TRANSFER_FINISHED;
                 case 7:
-                    return TRANSFER_UNJOINABLE_PEER;
                 case 8:
-                    return TRANSFER_UNJOINABLE_PEER;
-                case 9:
-                    return TRANSFER_ERROR;
                 case 10:
                     return TRANSFER_UNJOINABLE_PEER;
                 case 11:
diff --git a/ring-android/libringclient/src/main/java/cx/ring/model/Phone.java b/ring-android/libringclient/src/main/java/cx/ring/model/Phone.java
index d0d629dc4c2ff8201446e9faebc206f080d9ee4f..7c514658b1b5bdf8db15133838b639f655b4db12 100644
--- a/ring-android/libringclient/src/main/java/cx/ring/model/Phone.java
+++ b/ring-android/libringclient/src/main/java/cx/ring/model/Phone.java
@@ -43,7 +43,7 @@ public class Phone {
     public Phone(String number, int category, String label) {
         mNumberType = NumberType.UNKNOWN;
         mCategory = category;
-        mNumber = new Uri(number);
+        mNumber = Uri.fromString(number);
         mLabel = label;
     }
     public Phone(Uri number, int category, String label) {
@@ -55,7 +55,7 @@ public class Phone {
 
     public Phone(String number, int category, String label, NumberType numberType) {
         mNumberType = numberType;
-        mNumber = new Uri(number);
+        mNumber = Uri.fromString(number);
         mLabel = label;
         mCategory = category;
     }
@@ -79,7 +79,7 @@ public class Phone {
     }
 
     public void setNumber(String number) {
-        setNumber(new Uri(number));
+        setNumber(Uri.fromString(number));
     }
 
     public NumberType getNumbertype() {
diff --git a/ring-android/libringclient/src/main/java/cx/ring/model/SipCall.java b/ring-android/libringclient/src/main/java/cx/ring/model/SipCall.java
index cb1308d8c59c12e9b337b602ddaf113649f71900..00911714adbf00caab670afab6302fea571307df 100644
--- a/ring-android/libringclient/src/main/java/cx/ring/model/SipCall.java
+++ b/ring-android/libringclient/src/main/java/cx/ring/model/SipCall.java
@@ -20,11 +20,13 @@
 package cx.ring.model;
 
 
+import com.google.gson.JsonElement;
 import com.google.gson.JsonObject;
 
 import java.util.HashMap;
 import java.util.Locale;
 import java.util.Map;
+import java.util.Objects;
 
 import cx.ring.utils.ProfileChunk;
 import cx.ring.utils.StringUtils;
@@ -44,6 +46,7 @@ public class SipCall extends Interaction {
     public final static String KEY_VIDEO_MUTED = "VIDEO_MUTED";
     public final static String KEY_AUDIO_CODEC = "AUDIO_CODEC";
     public final static String KEY_VIDEO_CODEC = "VIDEO_CODEC";
+    public final static String KEY_REGISTERED_NAME = "REGISTERED_NAME";
     public final static String KEY_DURATION = "duration";
     public final static String KEY_CONF_ID = "CONF_ID";
 
@@ -56,6 +59,7 @@ public class SipCall extends Interaction {
     private CallStatus mCallStatus = CallStatus.NONE;
 
     private long timestampEnd = 0;
+    private Long duration = null;
     private boolean missed = true;
     private String mAudioCodec;
     private String mVideoCodec;
@@ -93,7 +97,7 @@ public class SipCall extends Interaction {
         mContact = interaction.getContact();
     }
 
-    public SipCall(String daemonId, String account, String contactNumber, Direction direction) {
+    public SipCall(String daemonId, String account, String contactNumber, Direction direction, long timestamp) {
         mDaemonId = daemonId == null ? null : Long.parseLong(daemonId);
         mIsIncoming = direction == Direction.INCOMING;
         mAccount = account;
@@ -105,7 +109,7 @@ public class SipCall extends Interaction {
     }
 
     public SipCall(String daemonId, Map<String, String> call_details) {
-        this(daemonId, call_details.get(KEY_ACCOUNT_ID), call_details.get(KEY_PEER_NUMBER), Direction.fromInt(Integer.parseInt(call_details.get(KEY_CALL_TYPE))));
+        this(daemonId, call_details.get(KEY_ACCOUNT_ID), call_details.get(KEY_PEER_NUMBER), Direction.fromInt(Integer.parseInt(call_details.get(KEY_CALL_TYPE))), System.currentTimeMillis());
         setCallState(CallStatus.fromString(call_details.get(KEY_CALL_STATE)));
         setDetails(call_details);
     }
@@ -130,17 +134,29 @@ public class SipCall extends Interaction {
     }
 
     public Long getDuration() {
-        return toJson(mExtraFlag).get(KEY_DURATION) == null ? 0 : toJson(mExtraFlag).get(KEY_DURATION).getAsLong();
+        if (duration == null) {
+            JsonElement element = toJson(mExtraFlag).get(KEY_DURATION);
+            if (element != null) {
+                duration = element.getAsLong();
+            }
+        }
+        return duration == null ? 0 : duration;
     }
 
     public void setDuration(Long value) {
-        JsonObject jsonObject = getExtraFlag();
-        jsonObject.addProperty(KEY_DURATION, value);
-        mExtraFlag = fromJson(jsonObject);
+        if (Objects.equals(value, duration))
+            return;
+        duration = value;
+        if (duration != null && duration != 0) {
+            JsonObject jsonObject = getExtraFlag();
+            jsonObject.addProperty(KEY_DURATION, value);
+            mExtraFlag = fromJson(jsonObject);
+            missed = false;
+        }
     }
 
     public String getDurationString() {
-        Long mDuration = getDuration() / 1000;
+        long mDuration = getDuration() / 1000;
         if (mDuration < 60) {
             return String.format(Locale.getDefault(), "%02d secs", mDuration);
         }
diff --git a/ring-android/libringclient/src/main/java/cx/ring/model/TextMessage.java b/ring-android/libringclient/src/main/java/cx/ring/model/TextMessage.java
index 36881cd70ce4d09f86c30ead86a2cc2a6d681251..b9ca64b398b3d512f9c14e0b73499d341e7954c8 100644
--- a/ring-android/libringclient/src/main/java/cx/ring/model/TextMessage.java
+++ b/ring-android/libringclient/src/main/java/cx/ring/model/TextMessage.java
@@ -45,6 +45,16 @@ public class TextMessage extends Interaction {
         mBody = message;
     }
 
+    public TextMessage(String author, String account, long timestamp, ConversationHistory conversation, String message, boolean isIncoming) {
+        mAuthor = author;
+        mAccount = account;
+        mTimestamp = timestamp;
+        mType = InteractionType.TEXT.toString();
+        mConversation = conversation;
+        mIsIncoming = isIncoming;
+        mBody = message;
+    }
+
     public TextMessage(Interaction interaction) {
         mId = interaction.getId();
         mAuthor = interaction.getAuthor();
diff --git a/ring-android/libringclient/src/main/java/cx/ring/model/TrustRequest.java b/ring-android/libringclient/src/main/java/cx/ring/model/TrustRequest.java
index ec87091b44033f2d5f83e9949445277b3997f540..bf85766f0272620c77a66a1e8ea0f47c5433a72f 100644
--- a/ring-android/libringclient/src/main/java/cx/ring/model/TrustRequest.java
+++ b/ring-android/libringclient/src/main/java/cx/ring/model/TrustRequest.java
@@ -21,46 +21,53 @@
 package cx.ring.model;
 
 import java.util.Map;
-import java.util.Random;
 
+import cx.ring.utils.StringUtils;
 import ezvcard.Ezvcard;
 import ezvcard.VCard;
 
 public class TrustRequest {
     private static final String TAG = TrustRequest.class.getSimpleName();
 
-    private String mAccountId;
+    private final String mAccountId;
     private String mContactUsername = null;
-    private String mContactId;
+    private final Uri mRequestUri;
+    private String mConversationId;
     private VCard mVcard;
     private String mMessage;
-    private long mTimestamp;
-    private int mUuid;
+    private final long mTimestamp;
     private boolean mUsernameResolved = false;
 
-    public TrustRequest(String accountId, String contact, long received, String payload) {
+    public TrustRequest(String accountId, Uri uri, long received, String payload, String conversationId) {
         mAccountId = accountId;
-        mContactId = contact;
+        mRequestUri = uri;
+        mConversationId = StringUtils.isEmpty(conversationId) ? null : conversationId;
         mTimestamp = received;
         mVcard = Ezvcard.parse(payload).first();
         mMessage = null;
-        mUuid = new Random().nextInt();
     }
 
     public TrustRequest(String accountId, Map<String, String> info) {
-        this(accountId, info.get("from"), Long.decode(info.get("received")) * 1000L, info.get("payload"));
+        this(accountId, Uri.fromId(info.get("from")), Long.decode(info.get("received")) * 1000L, info.get("payload"), info.get("conversationId"));
     }
 
-    public int getUuid() {
-        return mUuid;
+    public TrustRequest(String accountId, Uri contactUri, String conversationId) {
+        mAccountId = accountId;
+        mRequestUri = contactUri;
+        mConversationId = conversationId;
+        mTimestamp = 0;
     }
 
     public String getAccountId() {
         return mAccountId;
     }
 
-    public String getContactId() {
-        return mContactId;
+    public Uri getUri() {
+        return mRequestUri;
+    }
+
+    public String getConversationId() {
+        return mConversationId;
     }
 
     public String getFullname() {
@@ -73,7 +80,7 @@ public class TrustRequest {
 
     public String getDisplayname() {
         boolean hasUsername = mContactUsername != null && !mContactUsername.isEmpty();
-        return hasUsername ? mContactUsername : mContactId;
+        return hasUsername ? mContactUsername : mRequestUri.toString();
     }
 
     public boolean isNameResolved() {
@@ -104,4 +111,8 @@ public class TrustRequest {
     public void setMessage(String message) {
         mMessage = message;
     }
+
+    public void setConversationId(String conversationId) {
+        mConversationId = conversationId;
+    }
 }
diff --git a/ring-android/libringclient/src/main/java/cx/ring/model/Uri.java b/ring-android/libringclient/src/main/java/cx/ring/model/Uri.java
index 0c38a9168fc8aff1c7ea146684480ce907c1b0d1..4116fd8683b360e3fede883bd346b748a958b46c 100644
--- a/ring-android/libringclient/src/main/java/cx/ring/model/Uri.java
+++ b/ring-android/libringclient/src/main/java/cx/ring/model/Uri.java
@@ -20,81 +20,67 @@
 package cx.ring.model;
 
 import java.io.Serializable;
+import java.util.Objects;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
 import cx.ring.utils.StringUtils;
+import cx.ring.utils.Tuple;
 
 public class Uri implements Serializable {
 
-    private String mDisplayName = null;
-    private String mScheme = null;
-    private String mUsername = null;
-    private String mHost = null;
-    private String mPort = null;
+    private final String mScheme;
+    private final String mUsername;
+    private final String mHost;
+    private final String mPort;
 
     private static final Pattern ANGLE_BRACKETS_PATTERN = Pattern.compile("^\\s*([^<>]+)?\\s*<([^<>]+)>\\s*$");
-    private static final Pattern RING_ID_PATTERN = Pattern.compile("^\\p{XDigit}{40}$", Pattern.CASE_INSENSITIVE);
+    private static final Pattern HEX_ID_PATTERN = Pattern.compile("^\\p{XDigit}{40}$", Pattern.CASE_INSENSITIVE);
     private static final Pattern RING_URI_PATTERN = Pattern.compile("^\\s*(?:ring(?:[\\s\\:]+))?(\\p{XDigit}{40})(?:@ring\\.dht)?\\s*$", Pattern.CASE_INSENSITIVE);
     private static final Pattern URI_PATTERN = Pattern.compile("^\\s*(\\w+:)?(?:([\\w.]+)@)?(?:([\\d\\w\\.\\-]+)(?::(\\d+))?)\\s*$", Pattern.CASE_INSENSITIVE);
     public static final String RING_URI_SCHEME = "ring:";
     public static final String JAMI_URI_SCHEME = "jami:";
-    public static final String SWARM_URI_SCHEME = "swarm:";
+    public static final String SWARM_SCHEME = "swarm:";
 
     private static final String ipv4Pattern = "(([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\.){3}([01]?\\d\\d?|2[0-4]\\d|25[0-5])";
     private static final String ipv6Pattern = "([0-9a-f]{1,4}:){7}([0-9a-f]){1,4}";
     private static final Pattern VALID_IPV4_PATTERN = Pattern.compile(ipv4Pattern, Pattern.CASE_INSENSITIVE);
     private static final Pattern VALID_IPV6_PATTERN = Pattern.compile(ipv6Pattern, Pattern.CASE_INSENSITIVE);
 
-
-    public Uri(String uri) {
-        if (uri != null) {
-            parseUri(uri);
-        }
+    public Uri(String scheme, String user, String host, String port) {
+        mScheme = scheme;
+        mUsername = user;
+        mHost = host;
+        mPort = port;
     }
 
-    public String getRawUriString() {
-        if (isRingId()) {
-            if (isSwarm())
-                return getScheme() + getRawRingId();
-            return RING_URI_SCHEME + getRawRingId();
-        }
-
-        StringBuilder builder = new StringBuilder(64);
-        if (getUsername() != null && !getUsername().isEmpty()) {
-            builder.append(getUsername()).append("@");
-        }
-        if (getHost() != null) {
-            builder.append(getHost());
-        }
-        if (getPort() != null && !getPort().isEmpty()) {
-            builder.append(":").append(getPort());
-        }
-        return builder.toString();
+    public Uri(String scheme, String host) {
+        mScheme = scheme;
+        mUsername = null;
+        mHost = host;
+        mPort = null;
     }
 
-    public String getUriString() {
-        if (getDisplayName() == null || getDisplayName().isEmpty()) {
-            return getRawUriString();
+    static public Uri fromString(String uri) {
+        Matcher m = URI_PATTERN.matcher(uri);
+        if (m.find()) {
+            return new Uri(m.group(1), m.group(2), m.group(3), m.group(4));
+        } else {
+            return new Uri(null, null, uri, null);
         }
-        return getDisplayName() + " <" + getRawUriString() + ">";
-    }
-
-    @Override
-    public String toString() {
-        return getUriString();
     }
 
-    public boolean isSingleIp() {
-        return (getUsername() == null || getUsername().isEmpty()) && isIpAddress(getHost());
+    static public Tuple<Uri, String> fromStringWithName(String uriString) {
+        Matcher m = ANGLE_BRACKETS_PATTERN.matcher(uriString);
+        if (m.find()) {
+            return new Tuple<>(fromString(m.group(2)), m.group(1));
+        } else {
+            return new Tuple<>(fromString(uriString), null);
+        }
     }
 
-    public boolean isRingId() {
-        return (getHost() != null && RING_ID_PATTERN.matcher(getHost()).find())
-                || (getUsername() != null && RING_ID_PATTERN.matcher(getUsername()).find());
-    }
-    public boolean isSwarm() {
-        return SWARM_URI_SCHEME.equals(getScheme());
+    public static Uri fromId(String conversationId) {
+        return new Uri(null, null, conversationId, null);
     }
 
     public String getRawRingId() {
@@ -105,43 +91,51 @@ public class Uri implements Serializable {
         }
     }
 
-    private void parseUri(String uri) {
-        Matcher m = ANGLE_BRACKETS_PATTERN.matcher(uri);
-        if (m.find()) {
-            setDisplayName(m.group(1));
-            parseUriRaw(m.group(2));
-        } else {
-            parseUriRaw(uri);
-        }
+    public String getUri() {
+        if (isSwarm())
+            return getScheme() + getRawRingId();
+        if (isHexId())
+            return getRawRingId();
+        return toString();
     }
 
-    private void parseUriRaw(String uri) {
-        Matcher m = URI_PATTERN.matcher(uri);
-        if (m.find()) {
-            setScheme(m.group(1));
-            setUsername(m.group(2));
-            setHost(m.group(3));
-            setPort(m.group(4));
-        } else {
-            setHost(uri);
+    public String getRawUriString() {
+        if (isSwarm())
+            return getScheme() + getRawRingId();
+        if (isHexId()) {
+            return RING_URI_SCHEME + getRawRingId();
         }
+        return toString();
     }
 
-    public String getUri() {
-        if (isSwarm())
-            return getScheme() + getRawRingId();
-        if (isRingId())
-            return getRawRingId();
-        else {
-            StringBuilder builder = new StringBuilder(64);
-            if (!StringUtils.isEmpty(getUsername())) {
-                builder.append(getUsername()).append("@");
-            }
-            if (getHost() != null) {
-                builder.append(getHost());
-            }
-            return builder.toString();
+    @Override
+    public String toString() {
+        StringBuilder builder = new StringBuilder(64);
+        if (!StringUtils.isEmpty(mScheme)) {
+            builder.append(mScheme);
+        }
+        if (!StringUtils.isEmpty(mUsername)) {
+            builder.append(mUsername).append('@');
+        }
+        if (!StringUtils.isEmpty(mHost)) {
+            builder.append(mHost);
+        }
+        if (!StringUtils.isEmpty(mPort)) {
+            builder.append(':').append(mPort);
         }
+        return builder.toString();
+    }
+
+    public boolean isSingleIp() {
+        return (getUsername() == null || getUsername().isEmpty()) && isIpAddress(getHost());
+    }
+
+    public boolean isHexId() {
+        return (getHost() != null && HEX_ID_PATTERN.matcher(getHost()).find())
+                || (getUsername() != null && HEX_ID_PATTERN.matcher(getUsername()).find());
+    }
+    public boolean isSwarm() {
+        return SWARM_SCHEME.equals(getScheme());
     }
 
     @Override
@@ -153,12 +147,12 @@ public class Uri implements Serializable {
             return false;
         }
         Uri uo = (Uri) o;
-        return (getUsername() == null ? uo.getUsername() == null : getUsername().equals(uo.getUsername()))
-                && (getHost() == null ? uo.getHost() == null : getHost().equals(uo.getHost()));
+        return Objects.equals(getUsername(), uo.getUsername())
+                && Objects.equals(getHost(), uo.getHost());
     }
 
     public boolean isEmpty() {
-        return (getUsername() == null || getUsername().isEmpty()) && (getHost() == null || getHost().isEmpty());
+        return StringUtils.isEmpty(getUsername()) && StringUtils.isEmpty(getHost());
     }
 
     /**
@@ -171,7 +165,6 @@ public class Uri implements Serializable {
      * <code>false</code> otherwise.
      */
     public static boolean isIpAddress(String ipAddress) {
-
         Matcher m1 = VALID_IPV4_PATTERN.matcher(ipAddress);
         if (m1.matches()) {
             return true;
@@ -180,43 +173,20 @@ public class Uri implements Serializable {
         return m2.matches();
     }
 
-    public String getDisplayName() {
-        return mDisplayName;
-    }
-
-    public void setDisplayName(String displayName) {
-        this.mDisplayName = displayName;
-    }
-
     public String getScheme() {
         return mScheme;
     }
 
-    public void setScheme(String scheme) {
-        this.mScheme = scheme;
-    }
-
     public String getUsername() {
         return mUsername;
     }
 
-    public void setUsername(String username) {
-        this.mUsername = username;
-    }
-
     public String getHost() {
         return mHost;
     }
 
-    public void setHost(String host) {
-        this.mHost = host;
-    }
-
     public String getPort() {
         return mPort;
     }
 
-    public void setPort(String port) {
-        this.mPort = port;
-    }
 }
diff --git a/ring-android/libringclient/src/main/java/cx/ring/services/AccountService.java b/ring-android/libringclient/src/main/java/cx/ring/services/AccountService.java
index 3b6195dbc5b0b86f38d2117c4ac3620497bfdc21..4fd2f9d90d0b3d21656165834eefd4815098ac10 100644
--- a/ring-android/libringclient/src/main/java/cx/ring/services/AccountService.java
+++ b/ring-android/libringclient/src/main/java/cx/ring/services/AccountService.java
@@ -26,10 +26,15 @@ import com.google.gson.JsonObject;
 import com.google.gson.JsonParser;
 
 import java.io.File;
+import java.io.IOException;
 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;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -58,6 +63,7 @@ import cx.ring.model.DataTransfer;
 import cx.ring.model.DataTransferError;
 import cx.ring.model.Interaction;
 import cx.ring.model.Interaction.InteractionStatus;
+import cx.ring.model.SipCall;
 import cx.ring.model.TextMessage;
 import cx.ring.model.TrustRequest;
 import cx.ring.model.Uri;
@@ -67,6 +73,7 @@ import cx.ring.utils.Log;
 import cx.ring.utils.StringUtils;
 import cx.ring.utils.SwigNativeConverter;
 import cx.ring.utils.VCardUtils;
+import ezvcard.Ezvcard;
 import ezvcard.VCard;
 import io.reactivex.Completable;
 import io.reactivex.Maybe;
@@ -78,7 +85,7 @@ import io.reactivex.subjects.PublishSubject;
 import io.reactivex.subjects.Subject;
 
 /**
- * This service handles the accounts (Ring and SIP)
+ * This service handles the accounts
  * - Load and manage the accounts stored in the daemon
  * - Keep a local cache of the accounts
  * - handle the callbacks that are send by the daemon
@@ -112,7 +119,6 @@ public class AccountService {
     private boolean mHasSipAccount;
     private boolean mHasRingAccount;
 
-    private final HashMap<Long, DataTransfer> mDataTransfers = new HashMap<>();
     private DataTransfer mStartingTransfer = null;
 
     private final BehaviorSubject<List<Account>> accountsSubject = BehaviorSubject.create();
@@ -168,6 +174,7 @@ public class AccountService {
     }
 
     private final Subject<Message> incomingMessageSubject = PublishSubject.create();
+    private final Subject<Interaction> incomingSwarmMessageSubject = PublishSubject.create();
 
     private final Observable<TextMessage> incomingTextMessageSubject = incomingMessageSubject
             .flatMapMaybe(msg -> {
@@ -204,7 +211,7 @@ public class AccountService {
                     l.time = obj.get("time").getAsLong();
                     l.accountId = msg.accountId;
                     l.callId = msg.callId;
-                    l.peer = new Uri(msg.author);
+                    l.peer = Uri.fromId(msg.author);
                     return Maybe.just(l);
                 } catch (Exception e) {
                     Log.w(TAG, "Failed to receive geolocation", e);
@@ -291,6 +298,12 @@ public class AccountService {
         return incomingTextMessageSubject;
     }
 
+    public Observable<TextMessage> getIncomingSwarmMessages() {
+        return incomingSwarmMessageSubject
+                .filter(i -> i instanceof TextMessage)
+                .map(i -> (TextMessage) i);
+    }
+
     public Observable<Location> getLocationUpdates() {
         return incomingLocationSubject;
     }
@@ -364,7 +377,6 @@ public class AccountService {
                 account.setVolatileDetails(volatileAccountDetails);
             }
 
-
             if (account.isSip()) {
                 hasSip = true;
             } else if (account.isJami()) {
@@ -377,20 +389,56 @@ public class AccountService {
                 for (Map<String, String> requestInfo : requests) {
                     TrustRequest request = new TrustRequest(accountId, requestInfo);
                     account.addRequest(request);
-                    CallContact contact = account.getContactFromCache(request.getContactId());
-                    if (!contact.detailsLoaded) {
-                        mVCardService.loadVCardProfile(request.getVCard())
-                                .subscribeOn(Schedulers.computation())
-                                .subscribe(profile -> contact.setProfile(profile.first, profile.second));
+                }
+                Log.w(TAG, accountId + " loading conversations");
+                List<String> conversations = Ringservice.getConversations(account.getAccountID());
+                for (String conversationId : conversations) {
+                    Map<String, String> info = Ringservice.conversationInfos(accountId, conversationId);
+                    /*for (Map.Entry<String, String> i : info.entrySet()) {
+                        Log.w(TAG, "conversation info: " + i.getKey() + " " + i.getValue());
+                    }*/
+                    Conversation.Mode mode = Conversation.Mode.values()[Integer.parseInt(info.get("mode"))];
+                    Conversation conversation = account.newSwarm(conversationId, mode);
+                    conversation.setLastMessageRead(mHistoryService.getLastMessageRead(accountId, conversation.getUri()));
+                    for (Map<String, String> member : Ringservice.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"));
+                        CallContact contact = conversation.findContact(uri);
+                        if (contact == null) {
+                            contact = account.getContactFromCache(uri);
+                            conversation.addContact(contact);
+                        }
+                    }
+                    Log.w(TAG, accountId + " " + conversation.getUri().getRawUriString() + " " + conversation.getContacts().size());
+                    account.conversationStarted(conversation);
+                    //account.addSwarmConversation(conversationId, members);
+                }
+                for (Map<String, String> requestData : Ringservice.getConversationRequests(account.getAccountID()).toNative()) {
+                    /*for (Map.Entry<String, String> e : requestData.entrySet()) {
+                        Log.e(TAG, "Request: " + e.getKey() + " " + e.getValue());
+                    }*/
+                    String conversationId = requestData.get("id");
+                    Uri from = Uri.fromString(requestData.get("from"));
+                    TrustRequest request = account.getRequest(from);
+                    if (request != null) {
+                        request.setConversationId(conversationId);
+                    } else {
+                        account.addRequest(new TrustRequest(account.getAccountID(), from, conversationId));
                     }
-                    // If name is in cache this can be synchronous
-                    if (enabled)
-                        Ringservice.lookupAddress(accountId, "", request.getContactId());
                 }
+                mExecutor.execute(() -> {
+                    for (String conversationId : conversations)
+                        Ringservice.loadConversationMessages(accountId, conversationId, "", 2);
+                });
+
                 if (enabled) {
                     for (CallContact contact : account.getContacts().values()) {
                         if (!contact.isUsernameLoaded())
-                            Ringservice.lookupAddress(accountId, "", contact.getPrimaryUri().getRawRingId());
+                            Ringservice.lookupAddress(accountId, "", contact.getUri().getRawRingId());
                     }
                 }
             }
@@ -406,22 +454,6 @@ public class AccountService {
             }
         }
 
-        // migration to multi accounts
-        mHistoryService.migrateDatabase(accountIds);
-        mHistoryService.getMigrationStatus().firstOrError().subscribe(migrationStatus -> {
-            if (migrationStatus == HistoryService.MigrationStatus.SUCCESSFUL) {
-                mVCardService.migrateProfiles(accountIds);
-                for (String accountId : accountIds) {
-                    Account account = getAccount(accountId);
-                    if (account.isJami()) {
-                        mVCardService.migrateContact(account.getContacts(), accountId);
-                        migrateContactEvents(account, account.getContacts(), account.getRequestsMigration());
-                    }
-                }
-                mVCardService.deleteLegacyProfiles();
-            }
-        }, e -> Log.e(TAG, "Error completing profile migration", e));
-
         accountsSubject.onNext(newAccounts);
     }
 
@@ -618,7 +650,7 @@ public class AccountService {
                     Random r = new Random(System.currentTimeMillis());
                     int key = Math.abs(r.nextInt());
 
-                    Log.d(TAG, "sendProfile, vcard " + stringVCard);
+                    Log.d(TAG, "sendProfile, vcard " + callId);
 
                     while (i <= nbTotal) {
                         HashMap<String, String> chunk = new HashMap<>();
@@ -639,6 +671,35 @@ public class AccountService {
         mExecutor.execute(() -> Ringservice.setMessageDisplayed(accountId, contactId, messageId, 3));
     }
 
+    public Single<Conversation> startConversation(String accountId, Collection<String> initialMembers) {
+        Account account = getAccount(accountId);
+        return Single.fromCallable(() -> {
+            Log.w(TAG, "startConversation");
+            String id = Ringservice.startConversation(accountId);
+            Conversation conversation = account.getSwarm(id);//new Conversation(accountId, new Uri(id));
+            for (String member : initialMembers) {
+                Log.w(TAG, "addConversationMember " + member);
+                Ringservice.addConversationMember(accountId, id, member);
+                conversation.addContact(account.getContactFromCache(member));
+            }
+            account.conversationStarted(conversation);
+            Log.w(TAG, "loadConversationMessages");
+            Ringservice.loadConversationMessages(accountId, id, id, 2);
+            return conversation;
+        }).subscribeOn(Schedulers.from(mExecutor));
+    }
+
+    public void loadConversationHistory(String accountId, Uri conversationUri, String root, long n) {
+        Ringservice.loadConversationMessages(accountId, conversationUri.getRawRingId(), root, n);
+    }
+
+    public void sendConversationMessage(String accountId, Uri conversationUri, String txt) {
+        mExecutor.execute(() -> {
+            Log.w(TAG, "sendConversationMessages " + conversationUri.getRawRingId() + " : " + txt);
+            Ringservice.sendMessage(accountId, conversationUri.getRawRingId(), txt, "");
+        });
+    }
+
     /**
      * @return Account Ids list from Daemon
      */
@@ -1023,7 +1084,7 @@ public class AccountService {
                 }
             }
             account.removeRequest(from);
-            handleTrustRequest(accountId, from.getUri(), null, ContactType.INVITATION_ACCEPTED);
+            //handleTrustRequest(accountId, from, null, ContactType.INVITATION_ACCEPTED);
         }
         mExecutor.execute(() -> Ringservice.acceptTrustRequest(accountId, from.getRawRingId()));
     }
@@ -1032,67 +1093,32 @@ public class AccountService {
     /**
      * Handles adding contacts and is the initial point of conversation creation
      *
-     * @param accountId  the user's account id
+     * @param conversation    the user's account
      * @param contactUri the contacts raw string uri
      */
-    private void handleTrustRequest(String accountId, String contactUri, TrustRequest request, ContactType type) {
+    private void handleTrustRequest(Conversation conversation, Uri contactUri, TrustRequest request, ContactType type) {
         ContactEvent event = new ContactEvent();
         switch (type) {
             case ADDED:
                 break;
             case INVITATION_RECEIVED:
                 event.setStatus(Interaction.InteractionStatus.UNKNOWN);
-                event.setAuthor(contactUri);
+                event.setAuthor(contactUri.getRawRingId());
                 event.setTimestamp(request.getTimestamp());
                 break;
             case INVITATION_ACCEPTED:
                 event.setStatus(Interaction.InteractionStatus.SUCCESS);
-                event.setAuthor(contactUri);
+                event.setAuthor(contactUri.getRawRingId());
                 break;
             case INVITATION_DISCARDED:
-                mHistoryService.clearHistory(contactUri, accountId, true).subscribe();
+                mHistoryService.clearHistory(contactUri.getRawRingId(), conversation.getAccountId(), true).subscribe();
                 return;
             default:
                 return;
         }
-        insertContactEvent(accountId, contactUri, event);
-    }
-
-    /**
-     * Migrates contact events including trust requests to the database. This is necessary because the old database did not store contact events.
-     * Should only be called in the migration process.
-     *
-     * @param account  the user's account object
-     * @param contacts the user's contacts (if they exist)
-     * @param requests the user's trust requests (if they exist)
-     */
-    private void migrateContactEvents(Account account, Map<String, CallContact> contacts, Map<String, TrustRequest> requests) {
-        String accountId = account.getAccountID();
-        for (TrustRequest request : requests.values()) {
-            CallContact contact = account.getContactFromCache(request.getContactId());
-            ContactEvent event = new ContactEvent(contact, request);
-            insertContactEvent(accountId, contact.getPrimaryUri().getUri(), event);
-        }
-        for (CallContact contact : contacts.values()) {
-            ContactEvent event = new ContactEvent(contact);
-            insertContactEvent(accountId, contact.getPrimaryUri().getUri(), event);
-
-        }
+        mHistoryService.insertInteraction(conversation.getAccountId(), conversation, event).subscribe();
     }
 
-    /**
-     * Inserts a contact event, and if needed, a conversation, into the database
-     *
-     * @param accountId      the user's account ID
-     * @param participantUri the contact's string uri
-     * @param event          the contact event
-     */
-    private void insertContactEvent(String accountId, String participantUri, ContactEvent event) {
-        Conversation conversation = getAccount(accountId).getByUri(participantUri);
-        mHistoryService.insertInteraction(accountId, conversation, event).subscribe();
-    }
-
-
     private enum ContactType {
         ADDED, INVITATION_RECEIVED, INVITATION_ACCEPTED, INVITATION_DISCARDED
     }
@@ -1100,24 +1126,24 @@ public class AccountService {
     /**
      * Refuses and blocks a pending trust request
      */
-    public boolean discardTrustRequest(final String accountId, final Uri contact) {
+    public boolean discardTrustRequest(final String accountId, final Uri contactUri) {
         Account account = getAccount(accountId);
         boolean removed = false;
         if (account != null) {
-            removed = account.removeRequest(contact);
-            handleTrustRequest(accountId, contact.getUri(), null, ContactType.INVITATION_DISCARDED);
+            removed = account.removeRequest(contactUri);
+            mHistoryService.clearHistory(contactUri.getRawRingId(), accountId, true).subscribe();
         }
-        mExecutor.execute(() -> Ringservice.discardTrustRequest(accountId, contact.getRawRingId()));
+        mExecutor.execute(() -> Ringservice.discardTrustRequest(accountId, contactUri.getRawRingId()));
         return removed;
     }
 
     /**
      * Sends a new trust request
      */
-    public void sendTrustRequest(final String accountId, final String to, final Blob message) {
-        Log.i(TAG, "sendTrustRequest() " + accountId + " " + to);
-        handleTrustRequest(accountId, new Uri(to).getUri(), null, ContactType.ADDED);
-        mExecutor.execute(() -> Ringservice.sendTrustRequest(accountId, to, message == null ? new Blob() : message));
+    public void sendTrustRequest(Conversation conversation, final Uri to, final Blob message) {
+        Log.i(TAG, "sendTrustRequest() " + conversation.getAccountId() + " " + to);
+        handleTrustRequest(conversation, to, null, ContactType.ADDED);
+        mExecutor.execute(() -> Ringservice.sendTrustRequest(conversation.getAccountId(), to.getRawRingId(), message == null ? new Blob() : message));
     }
 
     /**
@@ -1125,7 +1151,7 @@ public class AccountService {
      */
     public void addContact(final String accountId, final String uri) {
         Log.i(TAG, "addContact() " + accountId + " " + uri);
-        handleTrustRequest(accountId, new Uri(uri).getUri(), null, ContactType.ADDED);
+        //handleTrustRequest(accountId, Uri.fromString(uri), null, ContactType.ADDED);
         mExecutor.execute(() -> Ringservice.addContact(accountId, uri));
     }
 
@@ -1198,7 +1224,7 @@ public class AccountService {
     }
 
     public void setPushNotificationToken(final String pushNotificationToken) {
-        Log.i(TAG, "setPushNotificationToken()");
+        //Log.i(TAG, "setPushNotificationToken()");
         mExecutor.execute(() -> Ringservice.setPushNotificationToken(pushNotificationToken));
     }
 
@@ -1216,7 +1242,7 @@ public class AccountService {
     }
 
     void registrationStateChanged(String accountId, String newState, int code, String detailString) {
-        Log.d(TAG, "registrationStateChanged: " + accountId + ", " + newState + ", " + code + ", " + detailString);
+        //Log.d(TAG, "registrationStateChanged: " + accountId + ", " + newState + ", " + code + ", " + detailString);
 
         Account account = getAccount(accountId);
         if (account == null) {
@@ -1253,7 +1279,7 @@ public class AccountService {
         if (account == null) {
             return;
         }
-        Log.d(TAG, "volatileAccountDetailsChanged: " + accountId + " " + details.size());
+        //Log.d(TAG, "volatileAccountDetailsChanged: " + accountId + " " + details.size());
         account.setVolatileDetails(details);
         accountSubject.onNext(account);
     }
@@ -1288,17 +1314,24 @@ public class AccountService {
         incomingMessageSubject.onNext(message);
     }
 
-    void accountMessageStatusChanged(String accountId, long messageId, String to, int status) {
-        Log.d(TAG, "accountMessageStatusChanged: " + accountId + ", " + messageId + ", " + to + ", " + status);
-        mHistoryService
-                .accountMessageStatusChanged(accountId, messageId, to, status)
-                .subscribe(textMessageSubject::onNext, e -> Log.e(TAG, "Error updating message: " + e.getLocalizedMessage()));
+    void accountMessageStatusChanged(String accountId, String conversationId, String messageId, String peer, int status) {
+        Log.d(TAG, "accountMessageStatusChanged: " + accountId + ", " + conversationId + ", " + messageId + ", " + peer + ", " + status);
+        if (StringUtils.isEmpty(conversationId)) {
+            mHistoryService
+                    .accountMessageStatusChanged(accountId, messageId, peer, status)
+                    .subscribe(textMessageSubject::onNext, e -> Log.e(TAG, "Error updating message: " + e.getLocalizedMessage()));
+        } else {
+            TextMessage msg = new TextMessage(peer, accountId, messageId, null, null);
+            msg.setStatus(status);
+            msg.setSwarmInfo(conversationId, messageId, null);
+            textMessageSubject.onNext(msg);
+        }
     }
 
-    public void composingStatusChanged(String accountId, String contactUri, int status) {
-        Log.d(TAG, "composingStatusChanged: " + accountId + ", " + contactUri + " " + status);
+    public void composingStatusChanged(String accountId, String conversationId, String contactUri, int status) {
+        Log.d(TAG, "composingStatusChanged: " + accountId + ", " + contactUri + ", " + conversationId + ", " + status);
         getAccountSingle(accountId)
-                .subscribe(account -> account.composingStatusChanged(new Uri(contactUri), Account.ComposingStatus.fromInt(status)));
+                .subscribe(account -> account.composingStatusChanged(conversationId, Uri.fromId(contactUri), Account.ComposingStatus.fromInt(status)));
     }
 
     void errorAlert(int alert) {
@@ -1366,24 +1399,30 @@ public class AccountService {
         mDeviceRevocationSubject.onNext(result);
     }
 
-    void incomingTrustRequest(String accountId, String from, String message, long received) {
-        Log.d(TAG, "incomingTrustRequest: " + accountId + ", " + from + ", " + received);
+    void incomingTrustRequest(String accountId, String conversationId, String from, String message, long received) {
+        Log.d(TAG, "incomingTrustRequest: " + accountId + ", " + conversationId + ", " + from + ", " + received);
 
         Account account = getAccount(accountId);
         if (account != null) {
-            TrustRequest request = new TrustRequest(accountId, from, received * 1000L, message);
+            Uri fromUri = Uri.fromString(from);
+            TrustRequest request = account.getRequest(fromUri);
+            if (request == null)
+                request = new TrustRequest(accountId, fromUri, received * 1000L, message, conversationId);
+            else
+                request.setVCard(Ezvcard.parse(message).first());
+
             VCard vcard = request.getVCard();
             if (vcard != null) {
-                CallContact contact = account.getContactFromCache(request.getContactId());
+                CallContact contact = account.getContactFromCache(fromUri);
                 if (!contact.detailsLoaded) {
-                    VCardUtils.savePeerProfileToDisk(vcard, accountId, from + ".vcf", mDeviceRuntimeService.provideFilesDir());
+                    // VCardUtils.savePeerProfileToDisk(vcard, accountId, from + ".vcf", mDeviceRuntimeService.provideFilesDir());
                     mVCardService.loadVCardProfile(vcard)
                             .subscribeOn(Schedulers.computation())
                             .subscribe(profile -> contact.setProfile(profile.first, profile.second));
                 }
             }
             account.addRequest(request);
-            handleTrustRequest(accountId, new Uri(from).getUri(), request, ContactType.INVITATION_RECEIVED);
+            // handleTrustRequest(account, Uri.fromString(from), request, ContactType.INVITATION_RECEIVED);
             if (account.isEnabled())
                 lookupAddress(accountId, "", from);
             incomingRequestsSubject.onNext(request);
@@ -1409,21 +1448,24 @@ public class AccountService {
     }
 
     void registeredNameFound(String accountId, int state, String address, String name) {
-        // Log.d(TAG, "registeredNameFound: " + accountId + ", " + state + ", " + name + ", " + address);
-
-        if (!StringUtils.isEmpty(address)) {
-            Account account = getAccount(accountId);
-            if (account != null) {
-                account.registeredNameFound(state, address, name);
+        try {
+            //Log.d(TAG, "registeredNameFound: " + accountId + ", " + state + ", " + name + ", " + address);
+            if (!StringUtils.isEmpty(address)) {
+                Account account = getAccount(accountId);
+                if (account != null) {
+                    account.registeredNameFound(state, address, name);
+                }
             }
-        }
 
-        RegisteredName r = new RegisteredName();
-        r.accountId = accountId;
-        r.address = address;
-        r.name = name;
-        r.state = state;
-        registeredNameSubject.onNext(r);
+            RegisteredName r = new RegisteredName();
+            r.accountId = accountId;
+            r.address = address;
+            r.name = name;
+            r.state = state;
+            registeredNameSubject.onNext(r);
+        } catch (Exception e) {
+            Log.w(TAG, "registeredNameFound exception", e);
+        }
     }
 
     public void userSearchEnded(String accountId, int state, String query, ArrayList<Map<String, String>> results) {
@@ -1447,21 +1489,187 @@ public class AccountService {
         searchResultSubject.onNext(r);
     }
 
-    public DataTransferError sendFile(final DataTransfer dataTransfer, File file) {
-        mStartingTransfer = dataTransfer;
+    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();
+        String type = message.get("type");
+        String author = message.get("author");
+        Uri authorUri = Uri.fromId(author);
 
-        DataTransferInfo dataTransferInfo = new DataTransferInfo();
-        dataTransferInfo.setAccountId(dataTransfer.getAccount());
-        dataTransferInfo.setPeer(dataTransfer.getPeerId());
-        dataTransferInfo.setPath(file.getPath());
-        dataTransferInfo.setDisplayName(dataTransfer.getDisplayName());
+        //Log.w(TAG, "addMessage2 " + type + " " + author + " id:" + id + " parents:" + parents);
 
-        Log.i(TAG, "sendFile() id=" + dataTransfer.getId() + " accountId=" + dataTransferInfo.getAccountId() + ", peer=" + dataTransferInfo.getPeer() + ", filePath=" + dataTransferInfo.getPath());
+        long timestamp = Long.parseLong(message.get("timestamp")) * 1000;
+        CallContact contact = conversation.findContact(authorUri);
+        if (contact == null) {
+            contact = account.getContactFromCache(authorUri);
+        }
+        Interaction interaction;
+        switch (type) {
+            case "member":
+                contact.setAddedDate(new Date(timestamp));
+                interaction = new ContactEvent(contact);
+                break;
+            case "text/plain":
+                interaction = new TextMessage(author, account.getAccountID(), timestamp, conversation, message.get("body"), !contact.isUser());
+                break;
+            case "application/data-transfer+json": {
+                String transferId = message.get("tid");
+                long tid = Long.parseUnsignedLong(transferId);
+                interaction = account.getDataTransfer(tid);
+                if (interaction == null) {
+                    interaction = new DataTransfer(tid, account.getAccountID(), author, contact.isUser(), timestamp, 0, 0);
+                }
+                break;
+            }
+            case "application/call-history+json":
+                interaction = new SipCall(null, account.getAccountID(), authorUri.getRawUriString(), contact.isUser() ? SipCall.Direction.OUTGOING : SipCall.Direction.INCOMING, timestamp);
+                ((SipCall) interaction).setDuration(Long.parseLong(message.get("duration")));
+                break;
+            case "merge":
+            default:
+                interaction = new Interaction();
+                interaction.setAccount(account.getAccountID());
+                interaction.setConversation(conversation);
+                interaction.setType(Interaction.InteractionType.INVALID);
+                break;
+        }
+        interaction.setContact(contact);
+        interaction.setSwarmInfo(conversation.getUri().getRawRingId(), id, parents);
+        if (conversation.addSwarmElement(interaction)) {
+            if (conversation.isVisible())
+                mHistoryService.setMessageRead(account.getAccountID(), conversation.getUri(), interaction.getMessageId());
+        }
+        return interaction;
+    }
+
+    public void conversationLoaded(String accountId, String conversationId, List<Map<String, String>> messages) {
         try {
-            return getDataTransferError(mExecutor.submit(() -> Ringservice.sendFile(dataTransferInfo, 0)).get());
-        } catch (Exception ignored) {
+            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);
+            }
+            account.conversationChanged();
+        } catch (Exception e) {
+            Log.e(TAG, "Exception loading message", e);
         }
-        return DataTransferError.UNKNOWN;
+    }
+
+    private enum ConversationMemberEvent {
+        Add, Join, Remove, Ban
+    }
+
+    public void conversationMemberEvent(String accountId, String conversationId, String peerUri, int event) {
+        Log.w(TAG, "ConversationCallback: conversationMemberEvent " + accountId + "/" + conversationId);
+        Account account = getAccount(accountId);
+        if (account == null) {
+            Log.w(TAG, "conversationMemberEvent: can't find account");
+            return;
+        }
+        Conversation conversation = account.getSwarm(conversationId);
+        Uri uri = Uri.fromId(peerUri);
+        switch (ConversationMemberEvent.values()[event])  {
+            case Add:
+            case Join: {
+                CallContact contact = account.getContactFromCache(uri);
+                conversation.addContact(contact);
+                break;
+            }
+            case Remove:
+            case Ban: {
+                CallContact contact = conversation.findContact(uri);
+                if (contact != null) {
+                    conversation.removeContact(contact);
+                }
+                break;
+            }
+        }
+    }
+
+    public void conversationReady(String accountId, String conversationId) {
+        Log.w(TAG, "ConversationCallback: conversationReady " + accountId + "/" + conversationId);
+        Account account = getAccount(accountId);
+        if (account == null) {
+            Log.w(TAG, "conversationReady: can't find account");
+            return;
+        }
+        StringMap info = Ringservice.conversationInfos(accountId, conversationId);
+        /*for (Map.Entry<String, String> i : info.entrySet()) {
+            Log.w(TAG, "conversation info: " + i.getKey() + " " + i.getValue());
+        }*/
+        int modeInt = Integer.parseInt(info.get("mode"));
+        Conversation.Mode mode = Conversation.Mode.values()[modeInt];
+        Conversation conversation = account.newSwarm(conversationId, mode);
+
+        for (Map<String, String> member : Ringservice.getConversationMembers(accountId, conversationId)) {
+            Uri uri = Uri.fromId(member.get("uri"));
+            CallContact contact = conversation.findContact(uri);
+            if (contact == null) {
+                contact = account.getContactFromCache(uri);
+                conversation.addContact(contact);
+            }
+        }
+        account.conversationStarted(conversation);
+    }
+
+    public void conversationRemoved(String accountId, String conversationId) {
+        Account account = getAccount(accountId);
+        if (account == null) {
+            Log.w(TAG, "conversationRemoved: can't find account");
+            return;
+        }
+        account.removeSwarm(conversationId);
+    }
+
+    public void conversationRequestReceived(String accountId, String conversationId, Map<String, String> metadata) {
+        Log.w(TAG, "ConversationCallback: conversationRequestReceived " + accountId + "/" + conversationId + " " + metadata.size());
+        Account account = getAccount(accountId);
+        if (account == null) {
+            Log.w(TAG, "conversationRequestReceived: can't find account");
+            return;
+        }
+        Uri contactUri = Uri.fromId(metadata.get("from"));
+        account.addRequest(new TrustRequest(account.getAccountID(), contactUri, conversationId));
+    }
+
+    public void messageReceived(String accountId, String conversationId, Map<String, String> message) {
+        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) {
+            account.conversationUpdated(conversation);
+            boolean isIncoming = !interaction.getContact().isUser();
+            if (isIncoming) {
+                incomingSwarmMessageSubject.onNext(interaction);
+            }
+        }
+    }
+
+    public Completable sendFile(final DataTransfer dataTransfer) {
+        return Completable.fromAction(() -> {
+            mStartingTransfer = dataTransfer;
+
+            DataTransferInfo dataTransferInfo = new DataTransferInfo();
+            dataTransferInfo.setAccountId(dataTransfer.getAccount());
+            dataTransferInfo.setConversationId(dataTransfer.getConversationId());
+            dataTransferInfo.setPeer(dataTransfer.getConversationId());
+            dataTransferInfo.setPath(dataTransfer.destination.getAbsolutePath());
+            dataTransferInfo.setDisplayName(dataTransfer.getDisplayName());
+
+            Log.i(TAG, "sendFile() id=" + dataTransfer.getId() + " accountId=" + dataTransferInfo.getAccountId() + ", peer=" + dataTransferInfo.getPeer() + ", filePath=" + dataTransferInfo.getPath());
+            DataTransferError err = getDataTransferError(Ringservice.sendFile(dataTransferInfo, 0));
+            if (err != DataTransferError.SUCCESS) {
+                throw new IOException(err.name());
+            }
+        }).subscribeOn(Schedulers.from(mExecutor));
     }
 
     public List<cx.ring.daemon.Message> getLastMessages(String accountId, long baseTime) {
@@ -1473,32 +1681,41 @@ public class AccountService {
         return new ArrayList<>();
     }
 
-    public void acceptFileTransfer(long id) {
-        acceptFileTransfer(getDataTransfer(id));
+    public void acceptFileTransfer(final String accountId, final Uri conversationUri, long id) {
+        Account account = getAccount(accountId);
+        if (account != null) {
+            Conversation conversation = account.getByUri(conversationUri);
+            acceptFileTransfer(conversation, account.getDataTransfer(id));
+        }
     }
 
-    public void acceptFileTransfer(DataTransfer transfer) {
+    public void acceptFileTransfer(Conversation conversation, DataTransfer transfer) {
         if (transfer == null)
             return;
-        File path = mDeviceRuntimeService.getTemporaryPath(transfer.getPeerId(), transfer.getStoragePath());
-        acceptFileTransfer(transfer.getDaemonId(), path.getAbsolutePath(), 0);
+        File path = mDeviceRuntimeService.getTemporaryPath(conversation.getUri().getRawRingId(), transfer.getStoragePath());
+        String conversationId = conversation.getUri().getRawRingId();
+        acceptFileTransfer(conversation.getAccountId(), conversationId, transfer.getDaemonId(), path.getAbsolutePath(), 0);
     }
 
-    private void acceptFileTransfer(final Long dataTransferId, final String filePath, final long offset) {
+    private void acceptFileTransfer(final String accountId, final String conversationId, final Long dataTransferId, final String filePath, long offset) {
         Log.i(TAG, "acceptFileTransfer() id=" + dataTransferId + ", path=" + filePath + ", offset=" + offset);
-        mExecutor.execute(() -> Ringservice.acceptFileTransfer(dataTransferId, filePath, offset));
+        mExecutor.execute(() -> Ringservice.acceptFileTransfer(accountId, conversationId, dataTransferId, filePath, offset));
     }
 
-    public void cancelDataTransfer(final Long dataTransferId) {
+    public void cancelDataTransfer(final String accountId, final String conversationId, long dataTransferId) {
         Log.i(TAG, "cancelDataTransfer() id=" + dataTransferId);
-        mExecutor.execute(() -> Ringservice.cancelDataTransfer(dataTransferId));
+        mExecutor.execute(() -> Ringservice.cancelDataTransfer(accountId, conversationId, dataTransferId));
     }
 
     private class DataTransferRefreshTask implements Runnable {
+        private final Account mAccount;
+        private final Conversation mConversation;
         private final DataTransfer mToUpdate;
         public ScheduledFuture<?> scheduledTask;
 
-        DataTransferRefreshTask(DataTransfer t) {
+        DataTransferRefreshTask(Account account, Conversation conversation, DataTransfer t) {
+            mAccount = account;
+            mConversation = conversation;
             mToUpdate = t;
         }
 
@@ -1506,7 +1723,7 @@ public class AccountService {
         public void run() {
             synchronized (mToUpdate) {
                 if (mToUpdate.getStatus() == Interaction.InteractionStatus.TRANSFER_ONGOING) {
-                    dataTransferEvent(mToUpdate.getDaemonId(), 5);
+                    dataTransferEvent(mAccount, mConversation, mToUpdate.getDaemonId(), 5);
                 } else {
                     scheduledTask.cancel(false);
                     scheduledTask = null;
@@ -1515,55 +1732,67 @@ public class AccountService {
         }
     }
 
-    void dataTransferEvent(final long transferId, int eventCode) {
+    void dataTransferEvent(String accountId, String conversationId, final long transferId, int eventCode) {
+        Account account = getAccount(accountId);
+        if (account != null) {
+            Conversation conversation = StringUtils.isEmpty(conversationId) ? null : account.getSwarm(conversationId);
+            if (conversation == null)
+                conversation = account.getByUri(conversationId);
+            if (conversation == null)
+                return;
+            dataTransferEvent(account, conversation, transferId, eventCode);
+        }
+    }
+    void dataTransferEvent(Account account, Conversation conversation, final long transferId, int eventCode) {
         Interaction.InteractionStatus transferStatus = getDataTransferEventCode(eventCode);
         Log.d(TAG, "Data Transfer " + transferStatus);
         DataTransferInfo info = new DataTransferInfo();
-        if (getDataTransferError(Ringservice.dataTransferInfo(transferId, info)) != DataTransferError.SUCCESS)
+        if (getDataTransferError(Ringservice.dataTransferInfo(account.getAccountID(), conversation.getUri().getRawRingId(), transferId, info)) != DataTransferError.SUCCESS)
             return;
 
-        Account account = getAccount(info.getAccountId());
-        Conversation c = account.getByUri(new Uri(info.getPeer()).getUri());
-
         boolean outgoing = info.getFlags() == 0;
-        DataTransfer transfer = mDataTransfers.get(transferId);
+        DataTransfer transfer = account.getDataTransfer(transferId);
         if (transfer == null) {
-
             if (outgoing && mStartingTransfer != null) {
                 transfer = mStartingTransfer;
                 mStartingTransfer = null;
             } else {
-                transfer = new DataTransfer(c, account.getAccountID(), info.getDisplayName(),
+                transfer = new DataTransfer(conversation, info.getPeer(), account.getAccountID(), info.getDisplayName(),
                         outgoing, info.getTotalSize(),
                         info.getBytesProgress(), transferId);
-
-                mHistoryService.insertInteraction(account.getAccountID(), c, transfer).blockingAwait();
+                if (conversation.isSwarm()) {
+                    transfer.setSwarmInfo(conversation.getUri().getRawRingId(), null, null);
+                } else {
+                    mHistoryService.insertInteraction(account.getAccountID(), conversation, transfer).blockingAwait();
+                }
             }
-            mDataTransfers.put(transferId, transfer);
+            account.putDataTransfer(transferId, transfer);
         } else synchronized (transfer) {
             InteractionStatus oldState = transfer.getStatus();
             if (oldState != transferStatus) {
                 if (transferStatus == Interaction.InteractionStatus.TRANSFER_ONGOING) {
-                    DataTransferRefreshTask task = new DataTransferRefreshTask(transfer);
+                    DataTransferRefreshTask task = new DataTransferRefreshTask(account, conversation, transfer);
                     task.scheduledTask = mExecutor.scheduleAtFixedRate(task,
                             DATA_TRANSFER_REFRESH_PERIOD,
                             DATA_TRANSFER_REFRESH_PERIOD, TimeUnit.MILLISECONDS);
                 } else if (transferStatus.isError()) {
                     if (!transfer.isOutgoing()) {
-                        File tmpPath = mDeviceRuntimeService.getTemporaryPath(transfer.getPeerId(), transfer.getStoragePath());
+                        File tmpPath = mDeviceRuntimeService.getTemporaryPath(conversation.getUri().getRawRingId(), transfer.getStoragePath());
                         tmpPath.delete();
                     }
                 } else if (transferStatus == (Interaction.InteractionStatus.TRANSFER_FINISHED)) {
                     if (!transfer.isOutgoing()) {
-                        File tmpPath = mDeviceRuntimeService.getTemporaryPath(transfer.getPeerId(), transfer.getStoragePath());
-                        File path = mDeviceRuntimeService.getConversationPath(transfer.getPeerId(), transfer.getStoragePath());
+                        File tmpPath = mDeviceRuntimeService.getTemporaryPath(conversation.getUri().getRawRingId(), transfer.getStoragePath());
+                        File path = mDeviceRuntimeService.getConversationPath(conversation.getUri().getRawRingId(), transfer.getStoragePath());
                         FileUtils.moveFile(tmpPath, path);
                     }
                 }
             }
             transfer.setStatus(transferStatus);
             transfer.setBytesProgress(info.getBytesProgress());
-            mHistoryService.updateInteraction(transfer, account.getAccountID()).subscribe();
+            if (!conversation.isSwarm()) {
+                mHistoryService.updateInteraction(transfer, account.getAccountID()).subscribe();
+            }
         }
 
         dataTransferSubject.onNext(transfer);
@@ -1580,21 +1809,16 @@ public class AccountService {
     }
 
     private static DataTransferError getDataTransferError(Long errorCode) {
-        DataTransferError dataTransferError = DataTransferError.UNKNOWN;
         if (errorCode == null) {
             Log.e(TAG, "getDataTransferError: invalid error code");
         } else {
             try {
-                dataTransferError = DataTransferError.values()[errorCode.intValue()];
+                return DataTransferError.values()[errorCode.intValue()];
             } catch (ArrayIndexOutOfBoundsException ignored) {
                 Log.e(TAG, "getDataTransferError: invalid data transfer error from daemon");
             }
         }
-        return dataTransferError;
-    }
-
-    public DataTransfer getDataTransfer(long id) {
-        return mDataTransfers.get(id);
+        return DataTransferError.UNKNOWN;
     }
 
     public Subject<DataTransfer> getDataTransfers() {
diff --git a/ring-android/libringclient/src/main/java/cx/ring/services/CallService.java b/ring-android/libringclient/src/main/java/cx/ring/services/CallService.java
index 6f5103ccd2a62b2e8c62b18c65be03ed0824b551..984004593777aa87bcaa3f027851430f7b69d3c2 100644
--- a/ring-android/libringclient/src/main/java/cx/ring/services/CallService.java
+++ b/ring-android/libringclient/src/main/java/cx/ring/services/CallService.java
@@ -43,6 +43,7 @@ import cx.ring.model.Conversation;
 import cx.ring.model.SipCall;
 import cx.ring.model.Uri;
 import cx.ring.utils.Log;
+import cx.ring.utils.StringUtils;
 import ezvcard.VCard;
 import io.reactivex.Completable;
 import io.reactivex.Observable;
@@ -128,7 +129,7 @@ public class CallService {
             List<Conference.ParticipantInfo> newInfo = new ArrayList<>(info.size());
             if (conference.isConference()) {
                 for (Map<String, String> i : info) {
-                    SipCall call = conference.findCallByContact(new Uri(i.get("uri")));
+                    SipCall call = conference.findCallByContact(Uri.fromString(i.get("uri")));
                     if (call != null) {
                         newInfo.add(new Conference.ParticipantInfo(call.getContact(), i));
                     } else {
@@ -138,7 +139,7 @@ public class CallService {
             } else {
                 Account account = mAccountService.getAccount(conference.getCall().getAccount());
                 for (Map<String, String> i : info)
-                    newInfo.add(new Conference.ParticipantInfo(account.getContactFromCache(new Uri(i.get("uri"))), i));
+                    newInfo.add(new Conference.ParticipantInfo(account.getContactFromCache(Uri.fromString(i.get("uri"))), i));
             }
             conference.setInfo(newInfo);
         }
@@ -226,25 +227,27 @@ public class CallService {
         return call == null ? Observable.error(new IllegalArgumentException()) : getCallUpdates(call);
     }*/
 
-    public Observable<SipCall> placeCallObservable(final String accountId, final String number, final boolean audioOnly) {
-        return placeCall(accountId, number, audioOnly)
+    public Observable<SipCall> placeCallObservable(final String accountId, final Uri conversationUri, final Uri number, final boolean audioOnly) {
+        return placeCall(accountId, conversationUri, number, audioOnly)
                 .flatMapObservable(this::getCallUpdates);
     }
 
-    public Single<SipCall> placeCall(final String account, final String number, final boolean audioOnly) {
+    public Single<SipCall> placeCall(final String account, final Uri conversationUri, final Uri number, final boolean audioOnly) {
         return Single.fromCallable(() -> {
             Log.i(TAG, "placeCall() thread running... " + number + " audioOnly: " + audioOnly);
 
             HashMap<String, String> volatileDetails = new HashMap<>();
             volatileDetails.put(SipCall.KEY_AUDIO_ONLY, String.valueOf(audioOnly));
 
-            String callId = Ringservice.placeCall(account, number, StringMap.toSwig(volatileDetails));
+            String callId = Ringservice.placeCall(account, number.getUri(), StringMap.toSwig(volatileDetails));
             if (callId == null || callId.isEmpty())
                 return null;
             if (audioOnly) {
                 Ringservice.muteLocalMedia(callId, "MEDIA_TYPE_VIDEO", true);
             }
             SipCall call = addCall(account, callId, number, SipCall.Direction.OUTGOING);
+            if (conversationUri.isSwarm())
+                call.setSwarmInfo(conversationUri.getRawRingId());
             call.muteVideo(audioOnly);
             updateConnectionCount();
             return call;
@@ -445,7 +448,7 @@ public class CallService {
 
     public SipCall getCurrentCallForContactId(String contactId) {
         for (SipCall call : currentCalls.values()) {
-            if (contactId.contains(call.getContact().getPhones().get(0).getNumber().toString())) {
+            if (contactId.contains(call.getContact().getPrimaryNumber())) {
                 return call;
             }
         }
@@ -459,15 +462,15 @@ public class CallService {
         }
     }
 
-    private SipCall addCall(String accountId, String callId, String from, SipCall.Direction direction) {
+    private SipCall addCall(String accountId, String callId, Uri from, SipCall.Direction direction) {
         synchronized (currentCalls) {
             SipCall call = currentCalls.get(callId);
             if (call == null) {
                 Account account = mAccountService.getAccount(accountId);
-                Uri fromUri = new Uri(from);
-                Conversation conversation = account.getByUri(fromUri);
-                CallContact contact = mContactService.findContact(account, fromUri);
-                call = new SipCall(callId, new Uri(from).getUri(), accountId, conversation, contact, direction);
+                CallContact contact = mContactService.findContact(account, from);
+                Uri conversationUri = contact.getConversationUri().blockingFirst();
+                Conversation conversation = conversationUri.equals(from) ? account.getByUri(from) : account.getSwarm(conversationUri.getRawRingId());
+                call = new SipCall(callId, from.getUri(), accountId, conversation, contact, direction);
                 currentCalls.put(callId, call);
             } else {
                 Log.w(TAG, "Call already existed ! " + callId + " " + from);
@@ -486,8 +489,6 @@ public class CallService {
             conference = new Conference(call);
             currentConferences.put(confId, conference);
             conferenceSubject.onNext(conference);
-        } else {
-            Log.w(TAG, "Conference already existed ! " + confId);
         }
         return conference;
     }
@@ -499,23 +500,24 @@ public class CallService {
             sipCall.setCallState(callState);
             sipCall.setDetails(Ringservice.getCallDetails(callId).toNative());
         } else if (callState !=  SipCall.CallStatus.OVER && callState !=  SipCall.CallStatus.FAILURE) {
-            Map<String, String> callDetails = Ringservice.getCallDetails(callId).toNative();
+            Map<String, String> callDetails = Ringservice.getCallDetails(callId);
             sipCall = new SipCall(callId, callDetails);
-            if (!callDetails.containsKey(SipCall.KEY_PEER_NUMBER)) {
+            if (StringUtils.isEmpty(sipCall.getContactNumber())) {
                 Log.w(TAG, "No number");
                 return null;
             }
+
             sipCall.setCallState(callState);
 
-            CallContact contact = mContactService.findContact(mAccountService.getAccount(sipCall.getAccount()), new Uri(sipCall.getContactNumber()));
-            String registeredName = callDetails.get("REGISTERED_NAME");
+            CallContact contact = mContactService.findContact(mAccountService.getAccount(sipCall.getAccount()), Uri.fromString(sipCall.getContactNumber()));
+            String registeredName = callDetails.get(SipCall.KEY_REGISTERED_NAME);
             if (registeredName != null && !registeredName.isEmpty()) {
                 contact.setUsername(registeredName);
             }
             sipCall.setContact(contact);
 
             Account account = mAccountService.getAccount(sipCall.getAccount());
-            sipCall.setConversation(account.getByUri(contact.getPrimaryUri()));
+            sipCall.setConversation(account.getByUri(contact.getUri()));
 
             currentCalls.put(callId, sipCall);
             updateConnectionCount();
@@ -560,7 +562,7 @@ public class CallService {
     void incomingCall(String accountId, String callId, String from) {
         Log.d(TAG, "incoming call: " + accountId + ", " + callId + ", " + from);
 
-        SipCall call = addCall(accountId, callId, from, SipCall.Direction.INCOMING);
+        SipCall call = addCall(accountId, callId, Uri.fromStringWithName(from).first, SipCall.Direction.INCOMING);
         callSubject.onNext(call);
         updateConnectionCount();
     }
diff --git a/ring-android/libringclient/src/main/java/cx/ring/services/ContactService.java b/ring-android/libringclient/src/main/java/cx/ring/services/ContactService.java
index 1229cc9b899bf8427fcff60cd71427bb2287155b..fb6e2699842b3ff161ea7f7608b60a2dbb00f6a6 100644
--- a/ring-android/libringclient/src/main/java/cx/ring/services/ContactService.java
+++ b/ring-android/libringclient/src/main/java/cx/ring/services/ContactService.java
@@ -88,7 +88,10 @@ public abstract class ContactService {
     }
 
     public Observable<CallContact> observeContact(String accountId, CallContact contact, boolean withPresence) {
-        Uri uri = contact.getPrimaryUri();
+        //Log.w(TAG, "observeContact " + accountId + " " + contact.getUri() + " " + contact.isUser());
+        if (contact.isUser())
+            withPresence = false;
+        Uri uri = contact.getUri();
         String uriString = uri.getRawUriString();
         synchronized (contact) {
             if (contact.getPresenceUpdates() == null) {
@@ -126,12 +129,19 @@ public abstract class ContactService {
     }
 
     public Observable<List<CallContact>> observeContact(String accountId, List<CallContact> contacts, boolean withPresence) {
-        if (contacts.size() == 1) {
-            return observeContact(accountId, contacts.get(0), withPresence).map(Collections::singletonList);
-        } else {
+        if (contacts.isEmpty()) {
+            return Observable.just(Collections.emptyList());
+        } /*else if (contacts.size() == 1 || contacts.size() == 2) {
+
+            return observeContact(accountId, contacts.get(contacts.size() - 1), withPresence).map(Collections::singletonList);
+        } */else {
             List<Observable<CallContact>> observables = new ArrayList<>(contacts.size());
-            for (CallContact contact : contacts)
-                observables.add(observeContact(accountId, contact, false));
+            for (CallContact contact : contacts) {
+                if (!contact.isUser())
+                    observables.add(observeContact(accountId, contact, withPresence));
+            }
+            if (observables.isEmpty())
+                return Observable.just(Collections.emptyList());
             return Observable.combineLatest(observables, a -> {
                 List<CallContact> obs = new ArrayList<>(a.length);
                 for (Object o : a)
@@ -141,22 +151,31 @@ public abstract class ContactService {
         }
     }
 
-    public Single<CallContact> getLoadedContact(String accountId, CallContact contact) {
-        return observeContact(accountId, contact, false)
+    public Single<CallContact> getLoadedContact(String accountId, CallContact contact, boolean withPresence) {
+        return observeContact(accountId, contact, withPresence)
                 .filter(c -> c.isUsernameLoaded() && c.detailsLoaded)
                 .firstOrError();
     }
+    public Single<CallContact> getLoadedContact(String accountId, CallContact contact) {
+        return getLoadedContact(accountId, contact, false);
+    }
 
-    public Single<List<CallContact>> getLoadedContact(String accountId, List<CallContact> contacts) {
-        return observeContact(accountId, contacts, false)
-                .filter(cts -> {
-                    for (CallContact c : cts) {
-                        if (!c.isUsernameLoaded() || !c.detailsLoaded)
-                            return false;
-                    }
-                    return true;
-                })
-                .firstOrError();
+    public Single<List<CallContact>> getLoadedContact(String accountId, List<CallContact> contacts, boolean withPresence) {
+        if (contacts.isEmpty())
+            return Single.just(Collections.emptyList());
+        return Observable.fromIterable(contacts)
+                .flatMapSingle(contact -> getLoadedContact(accountId, contact, withPresence))
+                .toList(contacts.size());
+    }
+
+    public List<Observable<CallContact>> observeLoadedContact(String accountId, List<CallContact> contacts, boolean withPresence) {
+        if (contacts.isEmpty())
+            return Collections.emptyList();
+        List<Observable<CallContact>> ret = new ArrayList<>(contacts.size());
+        for (CallContact contact : contacts)
+            ret.add(observeContact(accountId, contact, withPresence)
+                    .filter(c -> c.isUsernameLoaded() && c.detailsLoaded));
+        return ret;
     }
 
     /**
@@ -169,7 +188,7 @@ public abstract class ContactService {
         if (StringUtils.isEmpty(number) || account == null) {
             return null;
         }
-        return findContact(account, new Uri(number));
+        return findContact(account, Uri.fromString(number));
     }
 
     public CallContact findContact(Account account, Uri uri) {
diff --git a/ring-android/libringclient/src/main/java/cx/ring/services/DaemonService.java b/ring-android/libringclient/src/main/java/cx/ring/services/DaemonService.java
index 57b338a159c01c9975f1504cfeda5bbd5ddd4426..9871aee81ab016fa8832aebe9a19cbc30650e9f8 100644
--- a/ring-android/libringclient/src/main/java/cx/ring/services/DaemonService.java
+++ b/ring-android/libringclient/src/main/java/cx/ring/services/DaemonService.java
@@ -156,11 +156,11 @@ public class DaemonService {
 
         @Override
         public void accountProfileReceived(String account_id, String name, String photo) {
-            mExecutor.submit(() -> mAccountService.accountProfileReceived(account_id, name, photo));
+            mAccountService.accountProfileReceived(account_id, name, photo);
         }
 
         @Override
-        public void incomingAccountMessage(String accountId, String messageId, String from, StringMap messages) {
+        public void incomingAccountMessage(String accountId, String from, String messageId, StringMap messages) {
             if (messages == null || messages.isEmpty())
                 return;
             Map<String, String> jmessages = messages.toNativeFromUtf8();
@@ -168,13 +168,13 @@ public class DaemonService {
         }
 
         @Override
-        public void accountMessageStatusChanged(String accountId, long messageId, String to, int status) {
-            mExecutor.submit(() -> mAccountService.accountMessageStatusChanged(accountId, messageId, to, status));
+        public void accountMessageStatusChanged(String accountId, String conversationId, String peer, String messageId, int status) {
+            mExecutor.submit(() -> mAccountService.accountMessageStatusChanged(accountId, conversationId, messageId, peer, status));
         }
 
         @Override
-        public void composingStatusChanged(String accountId, String contactUri, int status) {
-            mExecutor.submit(() -> mAccountService.composingStatusChanged(accountId, contactUri, status));
+        public void composingStatusChanged(String accountId, String conversationId, String contactUri, int status) {
+            mExecutor.submit(() -> mAccountService.composingStatusChanged(accountId, conversationId, contactUri, status));
         }
 
         @Override
@@ -234,9 +234,9 @@ public class DaemonService {
         }
 
         @Override
-        public void incomingTrustRequest(String accountId, String from, Blob message, long received) {
+        public void incomingTrustRequest(String accountId, String conversationId, String from, Blob message, long received) {
             String jmessage = message.toJavaString();
-            mExecutor.submit(() -> mAccountService.incomingTrustRequest(accountId, from, jmessage, received));
+            mExecutor.submit(() -> mAccountService.incomingTrustRequest(accountId, conversationId, from, jmessage, received));
         }
 
         @Override
@@ -269,7 +269,7 @@ public class DaemonService {
 
         @Override
         public void remoteRecordingChanged(String call_id, String peer_number, boolean state) {
-            mCallService.remoteRecordingChanged(call_id, new Uri(peer_number), state);
+            mCallService.remoteRecordingChanged(call_id, Uri.fromString(peer_number), state);
         }
 
         @Override
@@ -380,35 +380,42 @@ public class DaemonService {
 
     class DaemonDataTransferCallback extends DataTransferCallback {
         @Override
-        public void dataTransferEvent(long transferId, int eventCode) {
-            Log.d(TAG, "dataTransferEvent: transferId=" + transferId + ", eventCode=" + eventCode);
-            mAccountService.dataTransferEvent(transferId, eventCode);
+        public void dataTransferEvent(String accountId, String conversationId, long transferId, int eventCode) {
+            Log.d(TAG, "dataTransferEvent: conversationId=" + conversationId + ", transferId=" + transferId + ", eventCode=" + eventCode);
+            mAccountService.dataTransferEvent(accountId, conversationId, transferId, eventCode);
         }
     }
 
     class ConversationCallbackImpl extends ConversationCallback {
         @Override
         public void conversationLoaded(long id, String accountId, String conversationId, VectMap messages) {
+            mAccountService.conversationLoaded(accountId, conversationId, messages.toNative());
         }
 
         @Override
         public void conversationReady(String accountId, String conversationId) {
+            mAccountService.conversationReady(accountId, conversationId);
         }
 
         @Override
         public void conversationRemoved(String accountId, String conversationId) {
+            mAccountService.conversationRemoved(accountId, conversationId);
         }
 
         @Override
         public void conversationRequestReceived(String accountId, String conversationId, StringMap metadata) {
+            mAccountService.conversationRequestReceived(accountId, conversationId, metadata.toNative());
         }
 
         @Override
         public void conversationMemberEvent(String accountId, String conversationId, String uri, int event) {
+            mAccountService.conversationMemberEvent(accountId, conversationId, uri, event);
         }
 
         @Override
         public void messageReceived(String accountId, String conversationId, StringMap message) {
+            mAccountService.messageReceived(accountId, conversationId, message.toNative());
         }
     }
+
 }
diff --git a/ring-android/libringclient/src/main/java/cx/ring/services/HistoryService.java b/ring-android/libringclient/src/main/java/cx/ring/services/HistoryService.java
index c68f4ea0c34a0424c720d75ed12c3f059541815d..0e475b479b8b6dcab2957c5105119a7e298ccf45 100644
--- a/ring-android/libringclient/src/main/java/cx/ring/services/HistoryService.java
+++ b/ring-android/libringclient/src/main/java/cx/ring/services/HistoryService.java
@@ -36,7 +36,6 @@ import cx.ring.model.Uri;
 import cx.ring.utils.Log;
 import cx.ring.utils.StringUtils;
 import io.reactivex.Completable;
-import io.reactivex.Observable;
 import io.reactivex.Scheduler;
 import io.reactivex.Single;
 import io.reactivex.schedulers.Schedulers;
@@ -56,6 +55,9 @@ public abstract class HistoryService {
 
     protected abstract void migrateDatabase(List<String> accounts);
 
+    public abstract void setMessageRead(String accountId, Uri conversationUri, String lastId);
+    public abstract String getLastMessageRead(String accountId, Uri conversationUri);
+
     protected abstract void deleteAccountHistory(String accountId);
 
     public abstract Observable<MigrationStatus> getMigrationStatus();
@@ -152,16 +154,11 @@ public abstract class HistoryService {
         return Completable.fromAction(() -> {
             Log.d(TAG, "Inserting interaction for account -> " + accountId);
             Dao<ConversationHistory, Integer> conversationDataDao = getConversationDataDao(accountId);
-
             ConversationHistory history = conversationDataDao.queryBuilder().where().eq(ConversationHistory.COLUMN_PARTICIPANT, conversation.getParticipant()).queryForFirst();
-
             if (history == null) {
                 history = conversationDataDao.createIfNotExists(new ConversationHistory(conversation.getParticipant()));
-                interaction.setConversation(history);
-            } else
-                interaction.setConversation(history);
-
-
+            }
+            //interaction.setConversation(conversation);
             conversation.setId(history.getId());
             getInteractionDataDao(accountId).create(interaction);
         })
@@ -185,7 +182,8 @@ public abstract class HistoryService {
                         "WHERE conversations.id = final.conversation\n" +
                         "GROUP BY final.conversation\n", (columnNames, resultColumns) -> new Interaction(resultColumns[0],
                         resultColumns[1], new ConversationHistory(Integer.parseInt(resultColumns[2]), resultColumns[12]), resultColumns[3], resultColumns[4], resultColumns[5], resultColumns[6], resultColumns[7], resultColumns[8], resultColumns[9])).getResults())
-                .subscribeOn(scheduler).doOnError(e -> Log.e(TAG, "Can't load smartlist from database", e))
+                .subscribeOn(scheduler)
+                .doOnError(e -> Log.e(TAG, "Can't load smartlist from database", e))
                 .onErrorReturn(e -> new ArrayList<>());
     }
 
@@ -212,20 +210,17 @@ public abstract class HistoryService {
 
     Single<TextMessage> incomingMessage(final String accountId, final String daemonId, final String from, final String message) {
         return Single.fromCallable(() -> {
-            String f = new Uri(from).getUri();
+            String fromUri = Uri.fromString(from).getUri();
             Dao<ConversationHistory, Integer> conversationDataDao = getConversationDataDao(accountId);
-
-            ConversationHistory conversation = conversationDataDao.queryBuilder().where().eq(ConversationHistory.COLUMN_PARTICIPANT, f).queryForFirst();
-
+            ConversationHistory conversation = conversationDataDao.queryBuilder().where().eq(ConversationHistory.COLUMN_PARTICIPANT, fromUri).queryForFirst();
             if (conversation == null) {
-                conversation = new ConversationHistory(f);
+                conversation = new ConversationHistory(fromUri);
                 conversation.setId(conversationDataDao.extractId(conversationDataDao.createIfNotExists(conversation)));
             }
 
-            TextMessage txt = new TextMessage(f, accountId, daemonId, conversation, message);
+            TextMessage txt = new TextMessage(fromUri, accountId, daemonId, conversation, message);
             txt.setStatus(Interaction.InteractionStatus.SUCCESS);
 
-
             Log.w(TAG, "New text messsage " + txt.getAuthor() + " " + txt.getDaemonId() + " " + txt.getBody());
             getInteractionDataDao(accountId).create(txt);
             return txt;
@@ -233,14 +228,14 @@ public abstract class HistoryService {
     }
 
 
-    Single<TextMessage> accountMessageStatusChanged(String accountId, long daemonId, String to, int status) {
+    Single<TextMessage> accountMessageStatusChanged(String accountId, String daemonId, String peer, int status) {
         return Single.fromCallable(() -> {
-            List<Interaction> textList = getInteractionDataDao(accountId).queryForEq(Interaction.COLUMN_DAEMON_ID, Long.toString(daemonId));
+            List<Interaction> textList = getInteractionDataDao(accountId).queryForEq(Interaction.COLUMN_DAEMON_ID, daemonId);
             if (textList == null || textList.isEmpty()) {
                 throw new RuntimeException("accountMessageStatusChanged: not able to find message with id " + daemonId + " in database");
             }
             Interaction text = textList.get(0);
-            String participant = (new Uri(to)).getUri();
+            String participant = Uri.fromString(peer).getUri();
             if (!text.getConversation().getParticipant().equals(participant)) {
                 throw new RuntimeException("accountMessageStatusChanged: received an invalid text message");
             }
@@ -251,4 +246,5 @@ public abstract class HistoryService {
             return msg;
         }).subscribeOn(scheduler);
     }
+
 }
\ No newline at end of file
diff --git a/ring-android/libringclient/src/main/java/cx/ring/services/NotificationService.java b/ring-android/libringclient/src/main/java/cx/ring/services/NotificationService.java
index d945a248cc5c50b0fdc927d5bf391bd26fc37c68..2ae71a52cad4149c195263c8671ccb3b8e63eb3f 100644
--- a/ring-android/libringclient/src/main/java/cx/ring/services/NotificationService.java
+++ b/ring-android/libringclient/src/main/java/cx/ring/services/NotificationService.java
@@ -44,7 +44,7 @@ public interface NotificationService {
 
     void cancelTextNotification(Uri contact);
 
-    void cancelTextNotification(String ringId);
+    void cancelTextNotification(String accountId, Uri conversationUri);
 
     void cancelAll();
 
@@ -52,7 +52,7 @@ public interface NotificationService {
 
     void cancelTrustRequestNotification(String accountID);
 
-    void showFileTransferNotification(DataTransfer info, CallContact contact);
+    void showFileTransferNotification(Conversation conversation, DataTransfer info);
 
     void showMissedCallNotification(SipCall call);
 
@@ -64,7 +64,7 @@ public interface NotificationService {
 
     void handleCallNotification(Conference conference, boolean remove);
 
-    void handleDataTransferNotification(DataTransfer transfer, CallContact contact, boolean remove);
+    void handleDataTransferNotification(DataTransfer transfer, Conversation contact, boolean remove);
 
     void removeTransferNotification(long transferId);
 
diff --git a/ring-android/libringclient/src/main/java/cx/ring/services/VCardService.java b/ring-android/libringclient/src/main/java/cx/ring/services/VCardService.java
index a07081f898a661436a0e92df6eeb7281063fe231..46fe2a84b772f97216c2f74552a176bdf12c3cb9 100644
--- a/ring-android/libringclient/src/main/java/cx/ring/services/VCardService.java
+++ b/ring-android/libringclient/src/main/java/cx/ring/services/VCardService.java
@@ -38,9 +38,6 @@ public abstract class VCardService {
     public abstract Single<Tuple<String, Object>> loadVCardProfile(VCard vcard);
     public abstract Single<Tuple<String, Object>> peerProfileReceived(String accountId, String peerId, File vcard);
 
-    public abstract void migrateContact(Map<String, CallContact> contacts, String accountId);
-    public abstract void migrateProfiles(List<String> accountIds);
-    public abstract void deleteLegacyProfiles();
     public abstract Object base64ToBitmap(String base64);
 
 }
diff --git a/ring-android/libringclient/src/main/java/cx/ring/smartlist/SmartListPresenter.java b/ring-android/libringclient/src/main/java/cx/ring/smartlist/SmartListPresenter.java
index 72530d30d52ee4964d74b129cda22a334d1f4ba9..315b9e46938c0df25dd6198cc26f0033935e12a1 100644
--- a/ring-android/libringclient/src/main/java/cx/ring/smartlist/SmartListPresenter.java
+++ b/ring-android/libringclient/src/main/java/cx/ring/smartlist/SmartListPresenter.java
@@ -103,7 +103,7 @@ public class SmartListPresenter extends RootPresenter<SmartListView> {
     }
 
     public void conversationClicked(SmartListViewModel viewModel) {
-        startConversation(viewModel.getAccountId(), viewModel.getContact());
+        startConversation(viewModel.getAccountId(), viewModel.getUri());
     }
 
     public void conversationLongClicked(SmartListViewModel smartListViewModel) {
@@ -118,10 +118,11 @@ public class SmartListPresenter extends RootPresenter<SmartListView> {
         getView().displayMenuItem();
     }
 
-    private void startConversation(String accountId, CallContact c) {
+    private void startConversation(String accountId, Uri conversationUri) {
+        Log.w(TAG, "startConversation " + accountId + " " + conversationUri);
         SmartListView view = getView();
-        if (view != null && c != null) {
-            view.goToConversation(accountId, c.getPrimaryUri());
+        if (view != null && conversationUri != null) {
+            view.goToConversation(accountId, conversationUri);
         }
     }
 
@@ -130,26 +131,26 @@ public class SmartListPresenter extends RootPresenter<SmartListView> {
     }
 
     public void copyNumber(SmartListViewModel smartListViewModel) {
-        getView().copyNumber(smartListViewModel.getContact());
+        getView().copyNumber(smartListViewModel.getUri());
     }
 
     public void clearConversation(SmartListViewModel smartListViewModel) {
-        getView().displayClearDialog(smartListViewModel.getContact());
+        getView().displayClearDialog(smartListViewModel.getUri());
     }
 
-    public void clearConversation(final CallContact callContact) {
+    public void clearConversation(final Uri uri) {
         mConversationDisposable.add(mConversationFacade
-                .clearHistory(mAccount.getAccountID(), callContact.getPrimaryUri())
+                .clearHistory(mAccount.getAccountID(), uri)
                 .subscribeOn(Schedulers.computation()).subscribe());
     }
 
     public void removeConversation(SmartListViewModel smartListViewModel) {
-        getView().displayDeleteDialog(smartListViewModel.getContact());
+        getView().displayDeleteDialog(smartListViewModel.getUri());
     }
 
-    public void removeConversation(CallContact callContact) {
+    public void removeConversation(Uri uri) {
         mConversationDisposable.add(mConversationFacade
-                .removeConversation(mAccount.getAccountID(), callContact.getPrimaryUri())
+                .removeConversation(mAccount.getAccountID(), uri)
                 .subscribeOn(Schedulers.computation()).subscribe());
     }
 
@@ -169,7 +170,7 @@ public class SmartListPresenter extends RootPresenter<SmartListView> {
                                 vms.add((SmartListViewModel) ob);
                             return vms;
                         }))
-                //.throttleLatest(150, TimeUnit.MILLISECONDS, mUiScheduler)
+                .throttleLatest(150, TimeUnit.MILLISECONDS, mUiScheduler)
                 .observeOn(mUiScheduler)
                 .subscribe(viewModels -> {
                     final SmartListView view = getView();
@@ -189,7 +190,8 @@ public class SmartListPresenter extends RootPresenter<SmartListView> {
     }
 
     public void banContact(SmartListViewModel smartListViewModel) {
-        CallContact contact = smartListViewModel.getContact();
-        mAccountService.removeContact(mAccount.getAccountID(), contact.getPrimaryNumber(), true);
+        //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/cx/ring/smartlist/SmartListView.java b/ring-android/libringclient/src/main/java/cx/ring/smartlist/SmartListView.java
index 11178ee8feeacd1c0b0189a7392891a750e6d50d..f678c3cf0e3b3e538c34944135a78163ec6dc5d4 100644
--- a/ring-android/libringclient/src/main/java/cx/ring/smartlist/SmartListView.java
+++ b/ring-android/libringclient/src/main/java/cx/ring/smartlist/SmartListView.java
@@ -33,11 +33,11 @@ public interface SmartListView extends BaseView {
 
     void displayConversationDialog(SmartListViewModel smartListViewModel);
 
-    void displayClearDialog(CallContact callContact);
+    void displayClearDialog(Uri callContact);
 
-    void displayDeleteDialog(CallContact callContact);
+    void displayDeleteDialog(Uri callContact);
 
-    void copyNumber(CallContact callContact);
+    void copyNumber(Uri uri);
 
     void setLoading(boolean display);
 
@@ -53,7 +53,7 @@ public interface SmartListView extends BaseView {
 
     void goToConversation(String accountId, Uri contactId);
 
-    void goToCallActivity(String accountId, String contactId);
+    void goToCallActivity(String accountId, Uri conversationUri, String contactId);
 
     void goToQRFragment();
 
diff --git a/ring-android/libringclient/src/main/java/cx/ring/smartlist/SmartListViewModel.java b/ring-android/libringclient/src/main/java/cx/ring/smartlist/SmartListViewModel.java
index 7847976a6756f9cf27e08e99c48ce062d9bc089a..b9354bc12a400d4c1571e7655b90876b9ab2a2e0 100644
--- a/ring-android/libringclient/src/main/java/cx/ring/smartlist/SmartListViewModel.java
+++ b/ring-android/libringclient/src/main/java/cx/ring/smartlist/SmartListViewModel.java
@@ -22,12 +22,12 @@ package cx.ring.smartlist;
 
 import java.util.Collections;
 import java.util.List;
+import java.util.Objects;
 
 import cx.ring.model.CallContact;
 import cx.ring.model.Conversation;
 import cx.ring.model.Interaction;
 import cx.ring.model.Uri;
-import cx.ring.services.AccountService;
 import io.reactivex.Observable;
 import io.reactivex.Single;
 
@@ -47,7 +47,7 @@ public class SmartListViewModel
     private boolean hasOngoingCall;
 
     private final boolean showPresence;
-    //private boolean isOnline = false;
+    private boolean isOnline = false;
     private boolean isChecked = false;
     private final Interaction lastEvent;
 
@@ -61,7 +61,7 @@ public class SmartListViewModel
     public SmartListViewModel(String accountId, CallContact contact, Interaction lastEvent) {
         this.accountId = accountId;
         this.contact = Collections.singletonList(contact);
-        this.uri = contact.getPrimaryUri();
+        this.uri = contact.getUri();
         uuid = uri.getRawUriString();
         this.contactName = contact.getDisplayName();
         hasUnreadTextMessage = (lastEvent != null) && !lastEvent.isRead();
@@ -74,14 +74,14 @@ public class SmartListViewModel
     public SmartListViewModel(String accountId, CallContact contact, String id, Interaction lastEvent) {
         this.accountId = accountId;
         this.contact = Collections.singletonList(contact);
-        uri = contact.getPrimaryUri();
+        uri = contact.getUri();
         this.uuid = id;
         this.contactName = contact.getDisplayName();
         hasUnreadTextMessage = (lastEvent != null) && !lastEvent.isRead();
         this.hasOngoingCall = false;
         this.lastEvent = lastEvent;
         showPresence = true;
-        //isOnline = contact.isOnline();
+        isOnline = contact.isOnline();
         title = Title.None;
     }
     public SmartListViewModel(Conversation conversation, List<CallContact> contacts, boolean presence) {
@@ -94,12 +94,20 @@ public class SmartListViewModel
         hasUnreadTextMessage = (lastEvent != null) && !lastEvent.isRead();
         this.hasOngoingCall = false;
         this.lastEvent = lastEvent;
+        for (CallContact contact : contacts) {
+            if (contact.isUser())
+                continue;
+            if (contact.isOnline()) {
+                isOnline = true;
+                break;
+            }
+        }
         //isOnline = contact.isOnline();
         showPresence = presence;
         title = Title.None;
     }
     public SmartListViewModel(Conversation conversation, boolean presence) {
-        this(conversation, Collections.singletonList(conversation.getContact()), presence);
+        this(conversation, conversation.getContacts(), presence);
     }
 
     private SmartListViewModel(Title title) {
@@ -118,8 +126,8 @@ public class SmartListViewModel
         return uri;
     }
 
-    public CallContact getContact() {
-        return contact == null ? null : contact.get(0);
+    public List<CallContact> getContact() {
+        return contact;
     }
 
     public String getContactName() {
@@ -177,8 +185,8 @@ public class SmartListViewModel
         return other.getHeaderTitle() == getHeaderTitle()
                 && (getHeaderTitle() != Title.None
                 || (contact == other.contact
-                && contactName.equals(other.contactName)
-                //&& isOnline == other.isOnline
+                && Objects.equals(contactName, other.contactName)
+                && isOnline == other.isOnline
                 && lastEvent == other.lastEvent
                 && hasOngoingCall == other.hasOngoingCall
                 && hasUnreadTextMessage == other.hasUnreadTextMessage));
diff --git a/ring-android/libringclient/src/main/java/cx/ring/utils/StringUtils.java b/ring-android/libringclient/src/main/java/cx/ring/utils/StringUtils.java
index 96701d49d01ef3a5c36f5c310b66359bdbabaeda..9756fa63a706476b1cacbe17d6eb77a15842d6f9 100644
--- a/ring-android/libringclient/src/main/java/cx/ring/utils/StringUtils.java
+++ b/ring-android/libringclient/src/main/java/cx/ring/utils/StringUtils.java
@@ -23,6 +23,7 @@ package cx.ring.utils;
 import java.util.Arrays;
 import java.util.HashSet;
 import java.util.Iterator;
+import java.util.List;
 
 public final class StringUtils {
 
@@ -120,4 +121,35 @@ public final class StringUtils {
         return true;
     }
 
+    public static String join(String separator, List<String> values) {
+        if (values.isEmpty()) return "";//need at least one element
+        if (values.size() == 1) return values.get(0);
+        //all string operations use a new array, so minimize all calls possible
+        char[] sep = separator.toCharArray();
+
+        // determine final size and normalize nulls
+        int totalSize = (values.size() - 1) * sep.length;// separator size
+        for (int i = 0; i < values.size(); i++) {
+            totalSize += values.get(i).length();
+        }
+
+        //exact size; no bounds checks or resizes
+        char[] joined = new char[totalSize];
+        int pos = 0;
+        //note, we are iterating all the elements except the last one
+        for (int i = 0, end = values.size()-1; i < end; i++) {
+            System.arraycopy(values.get(i).toCharArray(), 0,
+                    joined, pos, values.get(i).length());
+            pos += values.get(i).length();
+            System.arraycopy(sep, 0, joined, pos, sep.length);
+            pos += sep.length;
+        }
+        //now, add the last element;
+        //this is why we checked values.length == 0 off the hop
+        System.arraycopy(values.get(values.size()-1).toCharArray(), 0,
+                joined, pos, values.get(values.size()-1).length());
+
+        return new String(joined);
+    }
+
 }