From 79b6d163ad1e3c661f2b9c08449aba5409ef8c80 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Adrien=20B=C3=A9raud?= <adrien.beraud@savoirfairelinux.com>
Date: Tue, 8 Jun 2021 17:23:39 -0400
Subject: [PATCH] file transfer: update for new API

Change-Id: I24add1f0bcdb5847245dc66f84585bd65d6e7042
---
 build-daemon.sh                               |   2 +-
 .../cx/ring/adapters/ConversationAdapter.java |  75 +++++----
 .../ring/fragments/ConversationFragment.java  |  18 ++-
 .../java/cx/ring/service/DRingService.java    |   8 +-
 .../services/DeviceRuntimeServiceImpl.java    |   5 +
 .../services/NotificationServiceImpl.java     |  10 +-
 .../conversation/TvConversationAdapter.java   |   6 +-
 .../conversation/TvConversationFragment.java  |  10 +-
 .../java/cx/ring/utils/AndroidFileUtils.java  |  37 ++++-
 .../java/cx/ring/utils/ContentUriHandler.java |  28 ++--
 .../src/main/res/layout/item_conv_file_me.xml |   2 +-
 .../main/res/layout/item_conv_file_peer.xml   |   2 +-
 .../app/src/main/res/values/strings.xml       |   5 +-
 .../conversation/ConversationPresenter.java   |  15 +-
 .../jami/conversation/ConversationView.java   |   4 +-
 .../net/jami/facades/ConversationFacade.java  |  38 +++--
 .../src/main/java/net/jami/model/Account.java |   9 +-
 .../java/net/jami/model/Conversation.java     |   2 +-
 .../java/net/jami/model/DataTransfer.java     |  49 ++++--
 .../main/java/net/jami/model/Interaction.java |   3 +-
 .../net/jami/services/AccountService.java     | 150 +++++++++++-------
 .../java/net/jami/services/DaemonService.java |   6 +-
 .../jami/services/DeviceRuntimeService.java   |  21 +++
 .../jami/services/NotificationService.java    |   2 +-
 24 files changed, 325 insertions(+), 182 deletions(-)

diff --git a/build-daemon.sh b/build-daemon.sh
index b2b5bd669..8f0e04957 100755
--- a/build-daemon.sh
+++ b/build-daemon.sh
@@ -181,8 +181,8 @@ STATIC_LIBS_ALL="-llog -lOpenSLES -landroid \
                 -lyaml-cpp -ljsoncpp -lhttp_parser -lfmt\
                 -luuid -lz -ldl \
                 -lvpx -lopus -lspeex -lspeexdsp -lx264 \
-                -largon2 \
                 -lgit2 \
+                -largon2 \
                 -liconv"
 
 LIBRING_JNI_DIR=${ANDROID_APP_DIR}/app/src/main/libs/${ANDROID_ABI}
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 90227b291..9c516366e 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
@@ -442,31 +442,19 @@ public class ConversationAdapter extends RecyclerView.Adapter<ConversationViewHo
         if (interaction.getType() == (InteractionType.CONTACT))
             return false;
 
-        switch (item.getItemId()) {
-            case R.id.conv_action_download: {
-                presenter.saveFile(interaction);
-                break;
-            }
-            case R.id.conv_action_share: {
-                presenter.shareFile(interaction);
-                break;
-            }
-            case R.id.conv_action_open: {
-                presenter.openFile(interaction);
-                break;
-            }
-            case R.id.conv_action_delete: {
-                presenter.deleteConversationItem(interaction);
-                break;
-            }
-            case R.id.conv_action_cancel_message: {
-                presenter.cancelMessage(interaction);
-                break;
-            }
-            case R.id.conv_action_copy_text: {
-                addToClipboard((interaction).getBody());
-                break;
-            }
+        int itemId = item.getItemId();
+        if (itemId == R.id.conv_action_download) {
+            presenter.saveFile(interaction);
+        } else if (itemId == R.id.conv_action_share) {
+            presenter.shareFile(interaction);
+        } else if (itemId == R.id.conv_action_open) {
+            presenter.openFile(interaction);
+        } else if (itemId == R.id.conv_action_delete) {
+            presenter.deleteConversationItem(interaction);
+        } else if (itemId == R.id.conv_action_cancel_message) {
+            presenter.cancelMessage(interaction);
+        } else if (itemId == R.id.conv_action_copy_text) {
+            addToClipboard((interaction).getBody());
         }
         return true;
     }
@@ -487,8 +475,10 @@ public class ConversationAdapter extends RecyclerView.Adapter<ConversationViewHo
 
         viewHolder.mImage.setOnClickListener(v -> {
             Uri contentUri = ContentUriHandler.getUriForFile(v.getContext(), ContentUriHandler.AUTHORITY_FILES, path);
-            Intent i = new Intent(context, MediaViewerActivity.class);
-            i.setAction(Intent.ACTION_VIEW).setDataAndType(contentUri, "image/*").setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+            Intent i = new Intent(context, MediaViewerActivity.class)
+                    .setAction(Intent.ACTION_VIEW)
+                    .setDataAndType(contentUri, "image/*")
+                    .setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
             ActivityOptionsCompat options = ActivityOptionsCompat.
                     makeSceneTransitionAnimation(conversationFragment.getActivity(), viewHolder.mImage, "picture");
             conversationFragment.startActivityForResult(i, 3006, options.toBundle());
@@ -628,23 +618,24 @@ 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(interaction.getConversationId() == null ? interaction.getConversation().getParticipant() : interaction.getConversationId(), file.getStoragePath());
+
+        File path = presenter.getDeviceRuntimeService().getConversationPath(file);
         if (file.isComplete())
             file.setSize(path.length());
-
         String timeString = timestampToDetailString(viewHolder.itemView.getContext(), file.getTimestamp());
-        viewHolder.compositeDisposable.add(timestampUpdateTimer.subscribe(t -> {
-            if (file.getStatus() == InteractionStatus.TRANSFER_FINISHED) {
+        viewHolder.compositeDisposable.add(timestampUpdateTimer.subscribe(time -> {
+            InteractionStatus status = file.getStatus();
+            if (status == InteractionStatus.TRANSFER_FINISHED) {
                 viewHolder.mMsgDetailTxt.setText(String.format("%s - %s",
                         timeString, Formatter.formatFileSize(viewHolder.itemView.getContext(), file.getTotalSize())));
-            } else if (file.getStatus() == InteractionStatus.TRANSFER_ONGOING) {
+            } else if (status == InteractionStatus.TRANSFER_ONGOING) {
                 viewHolder.mMsgDetailTxt.setText(String.format("%s / %s - %s",
                         Formatter.formatFileSize(viewHolder.itemView.getContext(), file.getBytesProgress()), Formatter.formatFileSize(viewHolder.itemView.getContext(), file.getTotalSize()),
-                        ResourceMapper.getReadableFileTransferStatus(conversationFragment.getActivity(), file.getStatus())));
+                        ResourceMapper.getReadableFileTransferStatus(viewHolder.itemView.getContext(), status)));
             } else {
                 viewHolder.mMsgDetailTxt.setText(String.format("%s - %s - %s",
                         timeString, Formatter.formatFileSize(viewHolder.itemView.getContext(), file.getTotalSize()),
-                        ResourceMapper.getReadableFileTransferStatus(conversationFragment.getActivity(), file.getStatus())));
+                        ResourceMapper.getReadableFileTransferStatus(viewHolder.itemView.getContext(), status)));
             }
         }));
 
@@ -731,7 +722,8 @@ public class ConversationAdapter extends RecyclerView.Adapter<ConversationViewHo
         } else if (type == TransferMsgType.AUDIO) {
             configureAudio(viewHolder, path);
         } else {
-            if (file.getStatus().isError()) {
+            InteractionStatus status = file.getStatus();
+            if (status.isError()) {
                 viewHolder.mIcon.setImageResource(R.drawable.baseline_warning_24);
             } else {
                 viewHolder.mIcon.setImageResource(R.drawable.baseline_attach_file_24);
@@ -739,13 +731,18 @@ public class ConversationAdapter extends RecyclerView.Adapter<ConversationViewHo
 
             viewHolder.mMsgTxt.setText(file.getDisplayName());
 
-            if (file.getStatus() == InteractionStatus.TRANSFER_AWAITING_HOST) {
+            if (status == InteractionStatus.TRANSFER_AWAITING_HOST) {
+                viewHolder.btnRefuse.setVisibility(View.VISIBLE);
                 viewHolder.mAnswerLayout.setVisibility(View.VISIBLE);
                 viewHolder.btnAccept.setOnClickListener(v -> presenter.acceptFile(file));
                 viewHolder.btnRefuse.setOnClickListener(v -> presenter.refuseFile(file));
+            } else if (status == InteractionStatus.FILE_AVAILABLE) {
+                viewHolder.btnRefuse.setVisibility(View.GONE);
+                viewHolder.mAnswerLayout.setVisibility(View.VISIBLE);
+                viewHolder.btnAccept.setOnClickListener(v -> presenter.acceptFile(file));
             } else {
                 viewHolder.mAnswerLayout.setVisibility(View.GONE);
-                if (file.getStatus() == InteractionStatus.TRANSFER_ONGOING) {
+                if (status == InteractionStatus.TRANSFER_ONGOING) {
                     viewHolder.progress.setMax((int) (file.getTotalSize() / 1024));
                     if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
                         viewHolder.progress.setProgress((int) (file.getBytesProgress() / 1024), true);
@@ -1194,14 +1191,14 @@ public class ConversationAdapter extends RecyclerView.Adapter<ConversationViewHo
         FIRST,
         MIDDLE,
         LAST,
-        SINGLE;
+        SINGLE
     }
 
     private enum TransferMsgType {
         FILE,
         IMAGE,
         AUDIO,
-        VIDEO;
+        VIDEO
     }
     public enum MessageType {
         INCOMING_FILE(R.layout.item_conv_file_peer),
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 a832ac441..037ad09f0 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
@@ -759,23 +759,25 @@ public class ConversationFragment extends BaseSupportFragment<ConversationPresen
             return;
         }
         requireActivity().startService(new Intent(DRingService.ACTION_FILE_ACCEPT, ConversationPath.toUri(accountId, conversationUri), requireContext(), DRingService.class)
-                .putExtra(DRingService.KEY_TRANSFER_ID, transfer.getDaemonId()));
+                .putExtra(DRingService.KEY_MESSAGE_ID, transfer.getMessageId())
+                .putExtra(DRingService.KEY_TRANSFER_ID, transfer.getFileId()));
     }
 
     @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()));
+                .putExtra(DRingService.KEY_MESSAGE_ID, transfer.getMessageId())
+                .putExtra(DRingService.KEY_TRANSFER_ID, transfer.getFileId()));
     }
 
     @Override
-    public void shareFile(File path) {
+    public void shareFile(File path, String displayName) {
         Context c = getContext();
         if (c == null)
             return;
         android.net.Uri fileUri = null;
         try {
-            fileUri = ContentUriHandler.getUriForFile(c, ContentUriHandler.AUTHORITY_FILES, path);
+            fileUri = ContentUriHandler.getUriForFile(c, ContentUriHandler.AUTHORITY_FILES, path, displayName);
         } catch (IllegalArgumentException e) {
             Log.e("File Selector", "The selected file can't be shared: " + path.getName());
         }
@@ -783,7 +785,7 @@ public class ConversationFragment extends BaseSupportFragment<ConversationPresen
             Intent sendIntent = new Intent();
             sendIntent.setAction(Intent.ACTION_SEND);
             sendIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
-            String type = c.getContentResolver().getType(fileUri);
+            String type = c.getContentResolver().getType(fileUri.buildUpon().appendPath(displayName).build());
             sendIntent.setDataAndType(fileUri, type);
             sendIntent.putExtra(Intent.EXTRA_STREAM, fileUri);
             startActivity(Intent.createChooser(sendIntent, null));
@@ -791,13 +793,13 @@ public class ConversationFragment extends BaseSupportFragment<ConversationPresen
     }
 
     @Override
-    public void openFile(File path) {
+    public void openFile(File path, String displayName) {
         Context c = getContext();
         if (c == null)
             return;
         android.net.Uri fileUri = null;
         try {
-            fileUri = ContentUriHandler.getUriForFile(c, ContentUriHandler.AUTHORITY_FILES, path);
+            fileUri = ContentUriHandler.getUriForFile(c, ContentUriHandler.AUTHORITY_FILES, path, displayName);
         } catch (IllegalArgumentException e) {
             Log.e(TAG, "The selected file can't be shared: " + path.getName());
         }
@@ -805,7 +807,7 @@ public class ConversationFragment extends BaseSupportFragment<ConversationPresen
             Intent sendIntent = new Intent();
             sendIntent.setAction(Intent.ACTION_VIEW);
             sendIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
-            String type = c.getContentResolver().getType(fileUri);
+            String type = c.getContentResolver().getType(fileUri.buildUpon().appendPath(displayName).build());
             sendIntent.setDataAndType(fileUri, type);
             sendIntent.putExtra(Intent.EXTRA_STREAM, fileUri);
             //startActivity(Intent.createChooser(sendIntent, null));
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 319115791..743ac70b2 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
@@ -97,6 +97,7 @@ public class DRingService extends Service {
 
     static public final String ACTION_FILE_ACCEPT = BuildConfig.APPLICATION_ID + ".action.FILE_ACCEPT";
     static public final String ACTION_FILE_CANCEL = BuildConfig.APPLICATION_ID + ".action.FILE_CANCEL";
+    static public final String KEY_MESSAGE_ID = "messageId";
     static public final String KEY_TRANSFER_ID = "transferId";
     static public final String KEY_TEXT_REPLY = "textReply";
 
@@ -651,13 +652,14 @@ public class DRingService extends Service {
     }
 
     private void handleFileAction(android.net.Uri uri, String action, Bundle extras) {
-        long id = extras.getLong(KEY_TRANSFER_ID);
+        String messageId = extras.getString(KEY_MESSAGE_ID);
+        String id = extras.getString(KEY_TRANSFER_ID);
         ConversationPath path = ConversationPath.fromUri(uri);
         if (action.equals(ACTION_FILE_ACCEPT)) {
             mNotificationService.removeTransferNotification(path.getAccountId(), path.getConversationUri(), id);
-            mAccountService.acceptFileTransfer(path.getAccountId(), path.getConversationUri(), id);
+            mAccountService.acceptFileTransfer(path.getAccountId(), path.getConversationUri(), messageId, id);
         } else if (action.equals(ACTION_FILE_CANCEL)) {
-            mConversationFacade.cancelFileTransfer(path.getAccountId(), path.getConversationUri(), id);
+            mConversationFacade.cancelFileTransfer(path.getAccountId(), path.getConversationUri(), messageId, id);
         }
     }
 
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 07eda44a8..bdad5c9f8 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
@@ -97,6 +97,11 @@ public class DeviceRuntimeServiceImpl extends DeviceRuntimeService {
         return AndroidFileUtils.getConversationPath(mContext, conversationId, name);
     }
 
+    @Override
+    public File getConversationPath(String accountId, String conversationId, String name) {
+        return AndroidFileUtils.getConversationPath(mContext, accountId, conversationId, name);
+    }
+
     @Override
     public File getTemporaryPath(String conversationId, String name) {
         return AndroidFileUtils.getTempPath(mContext, conversationId, name);
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 fb4840dcb..9861ef901 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
@@ -471,12 +471,12 @@ public class NotificationServiceImpl implements NotificationService {
         if (!remove) {
             showFileTransferNotification(conversation, transfer);
         } else {
-            removeTransferNotification(ConversationPath.toUri(conversation), transfer.getDaemonId());
+            removeTransferNotification(ConversationPath.toUri(conversation), transfer.getFileId());
         }
     }
 
     @Override
-    public void removeTransferNotification(String accountId, Uri conversationUri, long transferId) {
+    public void removeTransferNotification(String accountId, Uri conversationUri, String transferId) {
         removeTransferNotification(ConversationPath.toUri(accountId, conversationUri), transferId);
     }
 
@@ -485,7 +485,7 @@ public class NotificationServiceImpl implements NotificationService {
      *
      * @param transferId the transfer id which is required to generate the notification id
      */
-    public void removeTransferNotification(android.net.Uri path, long transferId) {
+    public void removeTransferNotification(android.net.Uri path, String transferId) {
         int id = getFileTransferNotificationId(path, transferId);
         dataTransferNotifications.remove(id);
         cancelFileNotification(id, false);
@@ -749,7 +749,7 @@ public class NotificationServiceImpl implements NotificationService {
         }
         android.net.Uri path = ConversationPath.toUri(conversation);
         Log.d(TAG, "showFileTransferNotification " + path);
-        long dataTransferId = info.getDaemonId();
+        String dataTransferId = info.getFileId();
         int notificationId = getFileTransferNotificationId(path, dataTransferId);
 
         Intent intentConversation = new Intent(DRingService.ACTION_CONV_ACCEPT, path, mContext, DRingService.class);
@@ -955,7 +955,7 @@ public class NotificationServiceImpl implements NotificationService {
         return (NOTIF_MSG + accountId + contact.toString()).hashCode();
     }
 
-    private int getFileTransferNotificationId(android.net.Uri path, long dataTransferId) {
+    private int getFileTransferNotificationId(android.net.Uri path, String dataTransferId) {
         return (NOTIF_FILE_TRANSFER + path.toString() + dataTransferId).hashCode();
     }
 
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 be6428a5f..0eb649d75 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
@@ -632,13 +632,15 @@ public class TvConversationAdapter extends RecyclerView.Adapter<TvConversationVi
                 }
                 context.startService(new Intent(DRingService.ACTION_FILE_ACCEPT)
                         .setClass(context.getApplicationContext(), DRingService.class)
-                        .putExtra(DRingService.KEY_TRANSFER_ID, file.getDaemonId()));
+                        .putExtra(DRingService.KEY_MESSAGE_ID, file.getMessageId())
+                        .putExtra(DRingService.KEY_TRANSFER_ID, file.getFileId()));
             });
             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()));
+                        .putExtra(DRingService.KEY_MESSAGE_ID, file.getMessageId())
+                        .putExtra(DRingService.KEY_TRANSFER_ID, file.getFileId()));
             });
         } else {
             viewHolder.mAnswerLayout.setVisibility(View.GONE);
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 a0abdab81..0e93ea008 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
@@ -379,7 +379,7 @@ public class TvConversationFragment extends BaseSupportFragment<ConversationPres
     }
 
     @Override
-    public void shareFile(File path) {
+    public void shareFile(File path, String displayName) {
         Context c = getContext();
         if (c == null)
             return;
@@ -400,7 +400,7 @@ public class TvConversationFragment extends BaseSupportFragment<ConversationPres
     }
 
     @Override
-    public void openFile(File path) {
+    public void openFile(File path, String displayName) {
         Context c = getContext();
         if (c == null)
             return;
@@ -807,7 +807,8 @@ public class TvConversationFragment extends BaseSupportFragment<ConversationPres
         requireActivity().startService(new Intent(DRingService.ACTION_FILE_ACCEPT)
                 .setClass(requireContext(), DRingService.class)
                 .setData(ConversationPath.toUri(accountId, conversationUri))
-                .putExtra(DRingService.KEY_TRANSFER_ID, transfer.getDaemonId()));
+                .putExtra(DRingService.KEY_MESSAGE_ID, transfer.getMessageId())
+                .putExtra(DRingService.KEY_TRANSFER_ID, transfer.getFileId()));
     }
 
     @Override
@@ -815,7 +816,8 @@ public class TvConversationFragment extends BaseSupportFragment<ConversationPres
         requireActivity().startService(new Intent(DRingService.ACTION_FILE_CANCEL)
                 .setClass(requireContext(), DRingService.class)
                 .setData(ConversationPath.toUri(accountId, conversationUri))
-                .putExtra(DRingService.KEY_TRANSFER_ID, transfer.getDaemonId()));
+                .putExtra(DRingService.KEY_MESSAGE_ID, transfer.getMessageId())
+                .putExtra(DRingService.KEY_TRANSFER_ID, transfer.getFileId()));
     }
 
 }
diff --git a/ring-android/app/src/main/java/cx/ring/utils/AndroidFileUtils.java b/ring-android/app/src/main/java/cx/ring/utils/AndroidFileUtils.java
index f59739659..f57bf02cb 100644
--- a/ring-android/app/src/main/java/cx/ring/utils/AndroidFileUtils.java
+++ b/ring-android/app/src/main/java/cx/ring/utils/AndroidFileUtils.java
@@ -52,6 +52,7 @@ import java.util.Locale;
 
 import androidx.annotation.NonNull;
 
+import net.jami.model.Conversation;
 import net.jami.utils.FileUtils;
 
 import io.reactivex.Completable;
@@ -317,6 +318,22 @@ public class AndroidFileUtils {
         }).subscribeOn(Schedulers.io());
     }
 
+    public static @NonNull Single<File> getFileToSend(@NonNull Context context, @NonNull Conversation conversation, @NonNull Uri uri) {
+        ContentResolver contentResolver = context.getContentResolver();
+        File cacheDir = context.getCacheDir();
+        return Single.fromCallable(() -> {
+            File file = new File(cacheDir, getFilename(contentResolver, uri));
+            try (InputStream inputStream = contentResolver.openInputStream(uri);
+                 FileOutputStream output = new FileOutputStream(file)) {
+                if (inputStream == null)
+                    throw new FileNotFoundException();
+                net.jami.utils.FileUtils.copyFile(inputStream, output);
+                output.flush();
+            }
+            return file;
+        }).subscribeOn(Schedulers.io());
+    }
+
     public static Completable moveToUri(@NonNull ContentResolver cr, @NonNull File input, @NonNull Uri outUri) {
         return Completable.fromAction(() -> {
             try (InputStream inputStream = new FileInputStream(input);
@@ -363,7 +380,6 @@ public class AndroidFileUtils {
 
     public static File getConversationDir(Context context, String conversationId) {
         File conversationsDir = getFilePath(context, "conversation_data");
-
         if (!conversationsDir.exists())
             conversationsDir.mkdir();
 
@@ -374,9 +390,28 @@ public class AndroidFileUtils {
         return conversationDir;
     }
 
+    public static File getConversationDir(Context context, String accountId, String conversationId) {
+        File conversationsDir = getFilePath(context, "conversation_data");
+        if (!conversationsDir.exists())
+            conversationsDir.mkdir();
+
+        File accountDir = new File(conversationsDir, accountId);
+        if (!accountDir.exists())
+            accountDir.mkdir();
+
+        File conversationDir = new File(accountDir, conversationId);
+        if (!conversationDir.exists())
+            conversationDir.mkdir();
+
+        return conversationDir;
+    }
+
     public static File getConversationPath(Context context, String conversationId, String name) {
         return new File(getConversationDir(context, conversationId), name);
     }
+    public static File getConversationPath(Context context, String accountId, String conversationId, String name) {
+        return new File(getConversationDir(context, accountId, conversationId), name);
+    }
 
     public static File getTempPath(Context context, String conversationId, String name) {
         File conversationsDir = getCachePath(context, "conversation_data");
diff --git a/ring-android/app/src/main/java/cx/ring/utils/ContentUriHandler.java b/ring-android/app/src/main/java/cx/ring/utils/ContentUriHandler.java
index 9c1d51d8b..27f733902 100644
--- a/ring-android/app/src/main/java/cx/ring/utils/ContentUriHandler.java
+++ b/ring-android/app/src/main/java/cx/ring/utils/ContentUriHandler.java
@@ -24,12 +24,13 @@ import android.content.Context;
 import android.content.res.Resources;
 import android.net.Uri;
 import android.os.Build;
+import android.util.Log;
 
 import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 import androidx.core.content.FileProvider;
 
 import net.jami.utils.FileUtils;
-import net.jami.utils.Log;
 
 import java.io.File;
 
@@ -75,28 +76,33 @@ public class ContentUriHandler {
     }
 
     public static Uri getUriForFile(@NonNull Context context, @NonNull String authority, @NonNull File file) {
-        if (HUAWEI_MANUFACTURER.equalsIgnoreCase(Build.MANUFACTURER)) {
-            try {
-                return FileProvider.getUriForFile(context, authority, file);
-            } catch (IllegalArgumentException e) {
+        return getUriForFile(context, authority, file, null);
+    }
+    public static Uri getUriForFile(@NonNull Context context, @NonNull String authority, @NonNull File file, @Nullable String displayName) {
+        try {
+            return displayName == null ? FileProvider.getUriForFile(context, authority, file)
+                    : FileProvider.getUriForFile(context, authority, file, displayName);
+        } catch (IllegalArgumentException e) {
+            if (HUAWEI_MANUFACTURER.equalsIgnoreCase(Build.MANUFACTURER)) {
                 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
-                    net.jami.utils.Log.w(TAG, "Returning Uri.fromFile to avoid Huawei 'external-files-path' bug for pre-N devices", e);
+                    Log.w(TAG, "Returning Uri.fromFile to avoid Huawei 'external-files-path' bug for pre-N devices", e);
                     return Uri.fromFile(file);
                 } else {
-                    net.jami.utils.Log.w(TAG, "ANR Risk -- Copying the file the location cache to avoid Huawei 'external-files-path' bug for N+ devices", e);
+                    Log.w(TAG, "ANR Risk -- Copying the file the location cache to avoid Huawei 'external-files-path' bug for N+ devices", e);
                     // Note: Periodically clear this cache
                     final File cacheFolder = new File(context.getCacheDir(), HUAWEI_MANUFACTURER);
                     final File cacheLocation = new File(cacheFolder, file.getName());
                     if (FileUtils.copyFile(file, cacheLocation)) {
                         Log.i(TAG, "Completed Android N+ Huawei file copy. Attempting to return the cached file");
-                        return FileProvider.getUriForFile(context, authority, cacheLocation);
+                        return displayName == null ? FileProvider.getUriForFile(context, authority, cacheLocation)
+                                : FileProvider.getUriForFile(context, authority, cacheLocation, displayName);
                     }
-                    net.jami.utils.Log.e(TAG, "Failed to copy the Huawei file. Re-throwing exception");
+                    Log.e(TAG, "Failed to copy the Huawei file. Re-throwing exception");
                     throw new IllegalArgumentException("Huawei devices are unsupported for Android N");
                 }
+            } else {
+                throw e;
             }
-        } else {
-            return FileProvider.getUriForFile(context, authority, file);
         }
     }
 }
diff --git a/ring-android/app/src/main/res/layout/item_conv_file_me.xml b/ring-android/app/src/main/res/layout/item_conv_file_me.xml
index 6289f837a..17469f2b0 100644
--- a/ring-android/app/src/main/res/layout/item_conv_file_me.xml
+++ b/ring-android/app/src/main/res/layout/item_conv_file_me.xml
@@ -141,7 +141,7 @@
                         android:layout_weight="0.5"
                         android:background="?attr/selectableItemBackground"
                         android:gravity="center"
-                        android:text="@string/accept"
+                        android:text="@string/file_download"
                         android:textColor="@color/color_primary_dark" />
 
                 </LinearLayout>
diff --git a/ring-android/app/src/main/res/layout/item_conv_file_peer.xml b/ring-android/app/src/main/res/layout/item_conv_file_peer.xml
index b784d2fc4..251a58740 100644
--- a/ring-android/app/src/main/res/layout/item_conv_file_peer.xml
+++ b/ring-android/app/src/main/res/layout/item_conv_file_peer.xml
@@ -148,7 +148,7 @@
                         android:layout_weight="0.5"
                         android:background="?attr/selectableItemBackground"
                         android:gravity="center"
-                        android:text="@string/accept"
+                        android:text="@string/file_download"
                         android:textColor="@color/color_primary_dark" />
 
                 </LinearLayout>
diff --git a/ring-android/app/src/main/res/values/strings.xml b/ring-android/app/src/main/res/values/strings.xml
index d7c96cd03..1d2aa5260 100644
--- a/ring-android/app/src/main/res/values/strings.xml
+++ b/ring-android/app/src/main/res/values/strings.xml
@@ -241,8 +241,8 @@ along with this program; if not, write to the Free Software
     <string name="conversation_info_contact_you">You</string>
     <string name="conversation_members">Members</string>
 
-    <string name="conversation_preference_color">Change conversation color</string>
-    <string name="conversation_preference_emoji">Change conversation emoji</string>
+    <string name="conversation_preference_color">Conversation color</string>
+    <string name="conversation_preference_emoji">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>
@@ -337,6 +337,7 @@ along with this program; if not, write to the Free Software
     <string name="file_transfer_status_timed_out">timed out</string>
     <string name="file_saved_in">File saved in %s</string>
     <string name="file_saved_successfully">File saved successfully</string>
+    <string name="file_download">Download</string>
     <string name="no_space_left_on_device">No space left on device</string>
     <string name="title_media_viewer">Media viewer</string>
     <string name="menu_file_open">Open file</string>
diff --git a/ring-android/libringclient/src/main/java/net/jami/conversation/ConversationPresenter.java b/ring-android/libringclient/src/main/java/net/jami/conversation/ConversationPresenter.java
index 0e793ccc5..d106a517b 100644
--- a/ring-android/libringclient/src/main/java/net/jami/conversation/ConversationPresenter.java
+++ b/ring-android/libringclient/src/main/java/net/jami/conversation/ConversationPresenter.java
@@ -39,6 +39,7 @@ import net.jami.services.DeviceRuntimeService;
 import net.jami.services.HardwareService;
 import net.jami.services.PreferencesService;
 import net.jami.services.VCardService;
+import net.jami.utils.FileUtils;
 import net.jami.utils.Log;
 import net.jami.utils.StringUtils;
 import net.jami.utils.Tuple;
@@ -307,28 +308,24 @@ public class ConversationPresenter extends RootPresenter<ConversationView> {
     public void saveFile(Interaction interaction) {
         DataTransfer transfer = (DataTransfer) interaction;
         String fileAbsolutePath = getDeviceRuntimeService().
-                getConversationPath(mConversation.getUri().getRawRingId(), transfer.getStoragePath())
+                getConversationPath(transfer)
                 .getAbsolutePath();
         getView().startSaveFile(transfer, fileAbsolutePath);
     }
 
     public void shareFile(Interaction interaction) {
         DataTransfer file = (DataTransfer) interaction;
-        File path = getDeviceRuntimeService().getConversationPath(mConversation.getUri().getRawRingId(), file.getStoragePath());
-        getView().shareFile(path);
+        File path = getDeviceRuntimeService().getConversationPath(file);
+        getView().shareFile(path, file.getDisplayName());
     }
 
     public void openFile(Interaction interaction) {
         DataTransfer file = (DataTransfer) interaction;
-        File path = getDeviceRuntimeService().getConversationPath(mConversation.getUri().getRawRingId(), file.getStoragePath());
-        getView().openFile(path);
+        File path = getDeviceRuntimeService().getConversationPath(file);
+        getView().openFile(path, file.getDisplayName());
     }
 
     public void acceptFile(DataTransfer transfer) {
-        if (!getDeviceRuntimeService().hasWriteExternalStoragePermission()) {
-            getView().askWriteExternalStoragePermission();
-            return;
-        }
         getView().acceptFile(mConversation.getAccountId(), mConversationUri, transfer);
     }
 
diff --git a/ring-android/libringclient/src/main/java/net/jami/conversation/ConversationView.java b/ring-android/libringclient/src/main/java/net/jami/conversation/ConversationView.java
index e52b98d10..9194f0f8a 100644
--- a/ring-android/libringclient/src/main/java/net/jami/conversation/ConversationView.java
+++ b/ring-android/libringclient/src/main/java/net/jami/conversation/ConversationView.java
@@ -73,8 +73,8 @@ public interface ConversationView extends BaseView {
 
     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 shareFile(File path, String displayName);
+    void openFile(File path, String displayName);
 
     void addElement(Interaction e);
     void updateElement(Interaction e);
diff --git a/ring-android/libringclient/src/main/java/net/jami/facades/ConversationFacade.java b/ring-android/libringclient/src/main/java/net/jami/facades/ConversationFacade.java
index ec34df577..99bea15d3 100644
--- a/ring-android/libringclient/src/main/java/net/jami/facades/ConversationFacade.java
+++ b/ring-android/libringclient/src/main/java/net/jami/facades/ConversationFacade.java
@@ -38,6 +38,7 @@ import net.jami.services.HistoryService;
 import net.jami.services.NotificationService;
 import net.jami.services.PreferencesService;
 import net.jami.smartlist.SmartListViewModel;
+import net.jami.utils.FileUtils;
 import net.jami.utils.Log;
 import net.jami.utils.Tuple;
 
@@ -267,18 +268,21 @@ public class ConversationFacade {
     }
 
     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;
-            }
+        if (file == null || !file.exists() || !file.canRead()) {
+            Log.w(TAG, "sendFile: file not found or not readable: " + file);
+            return null;
+        }
 
-            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 (conversation.isSwarm()) {
+            File destPath = mDeviceRuntimeService.getNewConversationPath(conversation.getAccountId(), conversation.getUri().getRawRingId(), file.getName());
+            FileUtils.moveFile(file, destPath);
+            mAccountService.sendFile(conversation, destPath);
+            return Completable.complete();
+        }
+
+        return Single.fromCallable(() -> {
+            DataTransfer transfer = new DataTransfer(conversation, to.getRawRingId(), conversation.getAccountId(), file.getName(), true, file.length(), 0, null);
+            mHistoryService.insertInteraction(conversation.getAccountId(), conversation, transfer).blockingAwait();
 
             transfer.destination = mDeviceRuntimeService.getConversationDir(conversation.getUri().getRawRingId());
             return transfer;
@@ -297,7 +301,7 @@ public class ConversationFacade {
         if (element.getType() == Interaction.InteractionType.DATA_TRANSFER) {
             DataTransfer transfer = (DataTransfer) element;
             if (transfer.getStatus() == Interaction.InteractionStatus.TRANSFER_ONGOING) {
-                mAccountService.cancelDataTransfer(conversation.getAccountId(), conversation.getUri().getRawRingId(), transfer.getDaemonId());
+                mAccountService.cancelDataTransfer(conversation.getAccountId(), conversation.getUri().getRawRingId(), transfer.getMessageId(), transfer.getFileId());
             } else {
                 File file = mDeviceRuntimeService.getConversationPath(conversation.getUri().getRawRingId(), transfer.getStoragePath());
                 mDisposableBag.add(Completable.mergeArrayDelayError(
@@ -588,7 +592,7 @@ public class ConversationFacade {
         Conversation conversation = mAccountService.getAccount(transfer.getAccount()).onDataTransferEvent(transfer);
         if (transfer.getStatus() == Interaction.InteractionStatus.TRANSFER_CREATED && !transfer.isOutgoing()) {
             if (transfer.canAutoAccept(mPreferencesService.getMaxFileAutoAccept(transfer.getAccount()))) {
-                mAccountService.acceptFileTransfer(conversation, transfer);
+                mAccountService.acceptFileTransfer(conversation, transfer.getFileId(), transfer);
                 return;
             }
         }
@@ -678,10 +682,10 @@ public class ConversationFacade {
         });
     }
 
-    public void cancelFileTransfer(String accountId, Uri conversationId, long id) {
-        mAccountService.cancelDataTransfer(accountId, conversationId.isSwarm() ? conversationId.getRawRingId() : "", id);
-        mNotificationService.removeTransferNotification(accountId, conversationId, id);
-        DataTransfer transfer = mAccountService.getAccount(accountId).getDataTransfer(id);
+    public void cancelFileTransfer(String accountId, Uri conversationId, String messageId, String fileId) {
+        mAccountService.cancelDataTransfer(accountId, conversationId.isSwarm() ? conversationId.getRawRingId() : "", messageId, fileId);
+        mNotificationService.removeTransferNotification(accountId, conversationId, fileId);
+        DataTransfer transfer = mAccountService.getAccount(accountId).getDataTransfer(fileId);
         if (transfer != null)
             deleteConversationItem((Conversation) transfer.getConversation(), transfer);
     }
diff --git a/ring-android/libringclient/src/main/java/net/jami/model/Account.java b/ring-android/libringclient/src/main/java/net/jami/model/Account.java
index 934d36658..1ffe46f61 100644
--- a/ring-android/libringclient/src/main/java/net/jami/model/Account.java
+++ b/ring-android/libringclient/src/main/java/net/jami/model/Account.java
@@ -65,7 +65,7 @@ public class Account {
     private final Map<String, TrustRequest> mRequests = new HashMap<>();
     private final Map<String, Contact> mContactCache = new HashMap<>();
     private final Map<String, Conversation> swarmConversations = new HashMap<>();
-    private final HashMap<Long, DataTransfer> mDataTransfers = new HashMap<>();
+    private final HashMap<String, DataTransfer> mDataTransfers = new HashMap<>();
 
     private final Map<String, Conversation> conversations = new HashMap<>();
     private final Map<String, Conversation> pending = new HashMap<>();
@@ -379,6 +379,7 @@ public class Account {
     }
 
     public Conversation onDataTransferEvent(DataTransfer transfer) {
+        Log.d(TAG, "Account onDataTransferEvent " + transfer.getMessageId());
         Conversation conversation = (Conversation) transfer.getConversation();
         Interaction.InteractionStatus transferEventCode = transfer.getStatus();
         if (transferEventCode == Interaction.InteractionStatus.TRANSFER_CREATED) {
@@ -1120,12 +1121,12 @@ public class Account {
         mLoadedProfile = profile;
     }
 
-    public DataTransfer getDataTransfer(long id) {
+    public DataTransfer getDataTransfer(String id) {
         return mDataTransfers.get(id);
     }
 
-    public void putDataTransfer(long transferId, DataTransfer transfer) {
-        mDataTransfers.put(transferId, transfer);
+    public void putDataTransfer(String fileId, DataTransfer transfer) {
+        mDataTransfers.put(fileId, transfer);
     }
 
     private static class ConversationComparator implements Comparator<Conversation> {
diff --git a/ring-android/libringclient/src/main/java/net/jami/model/Conversation.java b/ring-android/libringclient/src/main/java/net/jami/model/Conversation.java
index 0aa215df5..db047ac28 100644
--- a/ring-android/libringclient/src/main/java/net/jami/model/Conversation.java
+++ b/ring-android/libringclient/src/main/java/net/jami/model/Conversation.java
@@ -694,7 +694,7 @@ public class Conversation extends ConversationHistory {
     }
 
     public void updateFileTransfer(DataTransfer transfer, Interaction.InteractionStatus eventCode) {
-        DataTransfer dataTransfer = (DataTransfer) findConversationElement(transfer.getId());
+        DataTransfer dataTransfer = (DataTransfer) (isSwarm() ? transfer : findConversationElement(transfer.getId()));
         if (dataTransfer != null) {
             dataTransfer.setStatus(eventCode);
             updatedElementSubject.onNext(new Tuple<>(dataTransfer, ElementStatus.UPDATE));
diff --git a/ring-android/libringclient/src/main/java/net/jami/model/DataTransfer.java b/ring-android/libringclient/src/main/java/net/jami/model/DataTransfer.java
index 079d8793f..31a8ceaeb 100644
--- a/ring-android/libringclient/src/main/java/net/jami/model/DataTransfer.java
+++ b/ring-android/libringclient/src/main/java/net/jami/model/DataTransfer.java
@@ -24,6 +24,7 @@ import net.jami.utils.HashUtils;
 import net.jami.utils.StringUtils;
 
 import java.io.File;
+import java.io.IOException;
 import java.util.Set;
 
 public class DataTransfer extends Interaction {
@@ -32,8 +33,9 @@ public class DataTransfer extends Interaction {
     private long mBytesProgress;
     //private final String mPeerId;
     private String mExtension;
-    //private String mTransferId;
+    private String mFileId;
     public File destination;
+    private File mDaemonPath;
 
     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");
@@ -41,7 +43,8 @@ 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 peer, String account, String displayName, boolean isOutgoing, long totalSize, long bytesProgress, long daemonId) {
+    /* Legacy constructor */
+    public DataTransfer(ConversationHistory conversation, String peer, String account, String displayName, boolean isOutgoing, long totalSize, long bytesProgress, String fileId) {
         mAuthor = isOutgoing ? null : peer;
         mAccount = account;
         mConversation = conversation;
@@ -52,8 +55,15 @@ public class DataTransfer extends Interaction {
         mType = InteractionType.DATA_TRANSFER.toString();
         mTimestamp = System.currentTimeMillis();
         mIsRead = 1;
-        mDaemonId = daemonId;
         mIsIncoming = !isOutgoing;
+        if (fileId != null) {
+            mFileId = fileId;
+            try {
+                mDaemonId = Long.parseUnsignedLong(fileId);
+            } catch (Exception e) {
+
+            }
+        }
     }
 
     public DataTransfer(Interaction interaction) {
@@ -72,18 +82,15 @@ public class DataTransfer extends Interaction {
         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;
+    public DataTransfer(String fileId, String accountId, String peerUri, String displayName, boolean isOutgoing, long timestamp, long totalSize, long bytesProgress) {
         mAccount = accountId;
-        //mTransferId = transferId;
-        //mPeerId = peerUri;
+        mFileId = fileId;
         mBody = displayName;
         mAuthor = peerUri;
         mIsIncoming = !isOutgoing;
         mTotalSize = totalSize;
         mBytesProgress = bytesProgress;
         mTimestamp = timestamp;
-        //mDaemonId = Long.parseUnsignedLong(transferId);
         mType = InteractionType.DATA_TRANSFER.toString();
     }
 
@@ -115,7 +122,7 @@ public class DataTransfer extends Interaction {
 
     public String getStoragePath() {
         if (StringUtils.isEmpty(mBody)) {
-            return getMessageId();
+            return getFileId();
         } else {
             String ext = StringUtils.getFileExtension(mBody);
             if (ext.length() > 8)
@@ -148,7 +155,8 @@ public class DataTransfer extends Interaction {
         return mBytesProgress;
     }
 
-    public void setBytesProgress(long bytesProgress) { mBytesProgress = bytesProgress;
+    public void setBytesProgress(long bytesProgress) {
+        mBytesProgress = bytesProgress;
     }
 
     public boolean isError() {
@@ -159,5 +167,26 @@ public class DataTransfer extends Interaction {
         return maxSize == UNLIMITED_SIZE || getTotalSize() <= maxSize;
     }
 
+    public String getFileId() {
+        return mFileId;
+    }
+
+    public void setDaemonPath(File file) {
+        mDaemonPath = file;
+    }
 
+    public File getDaemonPath() {
+        return mDaemonPath;
+    }
+
+    public File getPublicPath() {
+        if (mDaemonPath == null) {
+            return  null;
+        }
+        try {
+            return mDaemonPath.getCanonicalFile();
+        } catch (IOException e) {
+            return null;
+        }
+    }
 }
diff --git a/ring-android/libringclient/src/main/java/net/jami/model/Interaction.java b/ring-android/libringclient/src/main/java/net/jami/model/Interaction.java
index b4608a6e7..d7f2c02dc 100644
--- a/ring-android/libringclient/src/main/java/net/jami/model/Interaction.java
+++ b/ring-android/libringclient/src/main/java/net/jami/model/Interaction.java
@@ -242,7 +242,8 @@ public class Interaction {
         TRANSFER_AWAITING_PEER,
         TRANSFER_AWAITING_HOST,
         TRANSFER_TIMEOUT_EXPIRED,
-        TRANSFER_FINISHED;
+        TRANSFER_FINISHED,
+        FILE_AVAILABLE;
 
         static InteractionStatus fromString(String str) {
             for (InteractionStatus s : values()) {
diff --git a/ring-android/libringclient/src/main/java/net/jami/services/AccountService.java b/ring-android/libringclient/src/main/java/net/jami/services/AccountService.java
index 7568ece1c..f732512be 100644
--- a/ring-android/libringclient/src/main/java/net/jami/services/AccountService.java
+++ b/ring-android/libringclient/src/main/java/net/jami/services/AccountService.java
@@ -29,7 +29,6 @@ import net.jami.daemon.Blob;
 import net.jami.daemon.DataTransferInfo;
 import net.jami.daemon.JamiService;
 import net.jami.daemon.StringMap;
-import net.jami.daemon.StringVect;
 import net.jami.daemon.UintVect;
 import net.jami.model.Account;
 import net.jami.model.AccountConfig;
@@ -1494,18 +1493,16 @@ public class AccountService {
     }
 
     private Interaction addMessage(Account account, Conversation conversation, Map<String, String> message)  {
+        /* for (Map.Entry<String, String> e : message.entrySet()) {
+            Log.w(TAG, e.getKey() + " -> " + e.getValue());
+        } */
         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");
         String parent = message.get("linearizedParent");
         List<String> parents = StringUtils.isEmpty(parent) ? Collections.emptyList() : Collections.singletonList(parent);
         Uri authorUri = Uri.fromId(author);
 
-        //Log.w(TAG, "addMessage2 " + type + " " + author + " id:" + id + " parents:" + parents);
-
         long timestamp = Long.parseLong(message.get("timestamp")) * 1000;
         Contact contact = conversation.findContact(authorUri);
         if (contact == null) {
@@ -1522,19 +1519,24 @@ public class AccountService {
                 break;
             case "application/data-transfer+json": {
                 try {
-                    String transferId = message.get("tid");
-                    long tid = Long.parseLong(transferId);
                     String fileName = message.get("displayName");
-                    long fileSize = Long.parseLong(message.get("totalSize"));
-                    interaction = account.getDataTransfer(tid);
-                    if (interaction == null) {
-                        interaction = new DataTransfer(tid, account.getAccountID(), author, fileName, contact.isUser(), timestamp, fileSize, 0);
-                        File path = mDeviceRuntimeService.getConversationPath(conversation.getUri().getRawRingId(), ((DataTransfer) interaction).getStoragePath());
-                        boolean exists = path.exists();
-                        if (exists)
-                            ((DataTransfer) interaction).setBytesProgress(path.length());
-                        interaction.setStatus(exists ? InteractionStatus.TRANSFER_FINISHED : InteractionStatus.TRANSFER_TIMEOUT_EXPIRED);
-                    }
+                    String fileId = message.get("fileId");
+                    //interaction = account.getDataTransfer(fileId);
+                    //if (interaction == null) {
+                        String[] paths = new String[1];
+                        long[] progressA = new long[1];
+                        long[] totalA = new long[1];
+                        JamiService.fileTransferInfo(account.getAccountID(), conversation.getUri().getRawRingId(), fileId, paths, totalA, progressA);
+                        if (totalA[0] == 0) {
+                            totalA[0] = Long.parseLong(message.get("totalSize"));
+                        }
+                        File path = new File(paths[0]);
+                        interaction = new DataTransfer(fileId, account.getAccountID(), author, fileName, contact.isUser(), timestamp, totalA[0], progressA[0]);
+                        ((DataTransfer)interaction).setDaemonPath(path);
+                        boolean isComplete = path.exists() && progressA[0] == totalA[0];
+                        Log.w(TAG, "add DataTransfer at " + paths[0] + " with progress " + progressA[0] + "/" + totalA[0]);
+                        interaction.setStatus(isComplete ? InteractionStatus.TRANSFER_FINISHED : InteractionStatus.FILE_AVAILABLE);
+                    //}
                 } catch (Exception e) {
                     interaction = new Interaction(conversation, Interaction.InteractionType.INVALID);
                 }
@@ -1690,7 +1692,7 @@ public class AccountService {
 
             Log.i(TAG, "sendFile() id=" + dataTransfer.getId() + " accountId=" + dataTransferInfo.getAccountId() + ", peer=" + dataTransferInfo.getPeer() + ", filePath=" + dataTransferInfo.getPath());
             long[] id = new long[1];
-            DataTransferError err = getDataTransferError(JamiService.sendFile(dataTransferInfo, id));
+            DataTransferError err = getDataTransferError(JamiService.sendFileLegacy(dataTransferInfo, id));
             if (err != DataTransferError.SUCCESS) {
                 throw new IOException(err.name());
             } else {
@@ -1701,6 +1703,10 @@ public class AccountService {
         }).subscribeOn(Schedulers.from(mExecutor));
     }
 
+    public void sendFile(Conversation conversation, final File file) {
+        mExecutor.execute(() -> JamiService.sendFile(conversation.getAccountId(), conversation.getUri().getRawRingId(), file.getAbsolutePath(), file.getName(), ""));
+    }
+
     public List<net.jami.daemon.Message> getLastMessages(String accountId, long baseTime) {
         try {
             return mExecutor.submit(() -> SwigNativeConverter.toJava(JamiService.getLastMessages(accountId, baseTime))).get();
@@ -1710,30 +1716,33 @@ public class AccountService {
         return new ArrayList<>();
     }
 
-    public void acceptFileTransfer(final String accountId, final Uri conversationUri, long id) {
+    public void acceptFileTransfer(final String accountId, final Uri conversationUri, String messageId, String fileId) {
         Account account = getAccount(accountId);
         if (account != null) {
             Conversation conversation = account.getByUri(conversationUri);
-            acceptFileTransfer(conversation, account.getDataTransfer(id));
+            acceptFileTransfer(conversation, fileId, conversation.isSwarm() ? (DataTransfer)conversation.getMessage(messageId) : account.getDataTransfer(fileId));
         }
     }
 
-    public void acceptFileTransfer(Conversation conversation, DataTransfer transfer) {
-        if (transfer == null)
-            return;
-        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 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(() -> JamiService.acceptFileTransfer(accountId, conversationId, dataTransferId, filePath, offset));
+    public void acceptFileTransfer(Conversation conversation, String fileId, DataTransfer transfer) {
+        if (conversation.isSwarm()) {
+            String conversationId = conversation.getUri().getRawRingId();
+            File newPath = mDeviceRuntimeService.getNewConversationPath(conversation.getAccountId(), conversationId, transfer.getDisplayName());
+            Log.i(TAG, "downloadFile() id=" + conversation.getAccountId() + ", path=" + conversationId + " " + fileId + " to -> " + newPath.getAbsolutePath());
+            JamiService.downloadFile(conversation.getAccountId(), conversationId, transfer.getMessageId(), fileId, newPath.getAbsolutePath());
+        } else {
+            if (transfer == null) {
+                return;
+            }
+            File path = mDeviceRuntimeService.getTemporaryPath(conversation.getUri().getRawRingId(), transfer.getStoragePath());
+            Log.i(TAG, "acceptFileTransfer() id=" + fileId + ", path=" + path.getAbsolutePath());
+            JamiService.acceptFileTransfer(conversation.getAccountId(), fileId, path.getAbsolutePath());
+        }
     }
 
-    public void cancelDataTransfer(final String accountId, final String conversationId, long dataTransferId) {
-        Log.i(TAG, "cancelDataTransfer() id=" + dataTransferId);
-        mExecutor.execute(() -> JamiService.cancelDataTransfer(accountId, conversationId, dataTransferId));
+    public void cancelDataTransfer(final String accountId, final String conversationId, final String messageId, final String fileId) {
+        Log.i(TAG, "cancelDataTransfer() id=" + fileId);
+        mExecutor.execute(() -> JamiService.cancelDataTransfer(accountId, conversationId, fileId));
     }
 
     private class DataTransferRefreshTask implements Runnable {
@@ -1752,7 +1761,7 @@ public class AccountService {
         public void run() {
             synchronized (mToUpdate) {
                 if (mToUpdate.getStatus() == Interaction.InteractionStatus.TRANSFER_ONGOING) {
-                    dataTransferEvent(mAccount, mConversation, mToUpdate.getDaemonId(), 5);
+                    dataTransferEvent(mAccount, mConversation, mToUpdate.getMessageId(), mToUpdate.getFileId(), 5);
                 } else {
                     scheduledTask.cancel(false);
                     scheduledTask = null;
@@ -1761,41 +1770,69 @@ public class AccountService {
         }
     }
 
-    void dataTransferEvent(String accountId, String conversationId, final long transferId, int eventCode) {
+    void dataTransferEvent(String accountId, String conversationId, String interactionId, final String fileId, 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);
+            dataTransferEvent(account, conversation, interactionId, fileId, eventCode);
         }
     }
-    void dataTransferEvent(Account account, Conversation conversation, final long transferId, int eventCode) {
+    void dataTransferEvent(Account account, Conversation conversation, final String interactionId, final String fileId, int eventCode) {
         Interaction.InteractionStatus transferStatus = getDataTransferEventCode(eventCode);
-        Log.d(TAG, "Data Transfer " + transferStatus);
-        DataTransferInfo info = new DataTransferInfo();
-        if (getDataTransferError(JamiService.dataTransferInfo(account.getAccountID(), conversation.getUri().getRawRingId(), transferId, info)) != DataTransferError.SUCCESS)
-            return;
+        Log.d(TAG, "Data Transfer " + interactionId + " " + fileId + " " + transferStatus);
+
+        String from;
+        long total, progress;
+        String displayName;
+        DataTransfer transfer = account.getDataTransfer(fileId);
+        boolean outgoing = false;
+        if (conversation == null) {
+            DataTransferInfo info = new DataTransferInfo();
+            DataTransferError err = getDataTransferError(JamiService.dataTransferInfo(account.getAccountID(), fileId, info));
+            if (err != DataTransferError.SUCCESS) {
+                Log.d(TAG, "Data Transfer error getting details " + err);
+                return;
+            }
+            from = info.getPeer();
+            total = info.getTotalSize();
+            progress = info.getBytesProgress();
+            conversation = account.getByUri(from);
+            outgoing = info.getFlags() == 0;
+            displayName = info.getDisplayName();
+        } else {
+            String[] paths = new String[1];
+            long[] progressA = new long[1];
+            long[] totalA = new long[1];
+            JamiService.fileTransferInfo(account.getAccountID(), conversation.getUri().getRawRingId(), fileId, paths, totalA, progressA);
+            progress = progressA[0];
+            total = totalA[0];
+            if (transfer == null && !StringUtils.isEmpty(interactionId)) {
+                transfer = (DataTransfer) conversation.getMessage(interactionId);
+            }
+            if (transfer == null)
+                return;
+            transfer.setConversation(conversation);
+            transfer.setDaemonPath(new File(paths[0]));
+            from = transfer.getAuthor();
+            displayName = transfer.getDisplayName();
+        }
 
-        boolean outgoing = info.getFlags() == 0;
-        DataTransfer transfer = account.getDataTransfer(transferId);
         if (transfer == null) {
             if (outgoing && mStartingTransfer != null) {
+                Log.d(TAG, "Data Transfer mStartingTransfer");
                 transfer = mStartingTransfer;
                 mStartingTransfer = null;
             } else {
-                transfer = new DataTransfer(conversation, info.getPeer(), account.getAccountID(), info.getDisplayName(),
-                        outgoing, info.getTotalSize(),
-                        info.getBytesProgress(), transferId);
+                transfer = new DataTransfer(conversation, from, account.getAccountID(), displayName,
+                        outgoing, total,
+                        progress, fileId);
                 if (conversation.isSwarm()) {
-                    transfer.setSwarmInfo(conversation.getUri().getRawRingId(), null, null);
+                    transfer.setSwarmInfo(conversation.getUri().getRawRingId(), interactionId, null);
                 } else {
                     mHistoryService.insertInteraction(account.getAccountID(), conversation, transfer).blockingAwait();
                 }
             }
-            account.putDataTransfer(transferId, transfer);
+            account.putDataTransfer(fileId, transfer);
         } else synchronized (transfer) {
             InteractionStatus oldState = transfer.getStatus();
             if (oldState != transferStatus) {
@@ -1810,7 +1847,7 @@ public class AccountService {
                         tmpPath.delete();
                     }
                 } else if (transferStatus == (Interaction.InteractionStatus.TRANSFER_FINISHED)) {
-                    if (!transfer.isOutgoing()) {
+                    if (!conversation.isSwarm() && !transfer.isOutgoing()) {
                         File tmpPath = mDeviceRuntimeService.getTemporaryPath(conversation.getUri().getRawRingId(), transfer.getStoragePath());
                         File path = mDeviceRuntimeService.getConversationPath(conversation.getUri().getRawRingId(), transfer.getStoragePath());
                         FileUtils.moveFile(tmpPath, path);
@@ -1818,12 +1855,13 @@ public class AccountService {
                 }
             }
             transfer.setStatus(transferStatus);
-            transfer.setBytesProgress(info.getBytesProgress());
+            transfer.setBytesProgress(progress);
             if (!conversation.isSwarm()) {
                 mHistoryService.updateInteraction(transfer, account.getAccountID()).subscribe();
             }
         }
 
+        Log.d(TAG, "Data Transfer dataTransferSubject.onNext");
         dataTransferSubject.onNext(transfer);
     }
 
diff --git a/ring-android/libringclient/src/main/java/net/jami/services/DaemonService.java b/ring-android/libringclient/src/main/java/net/jami/services/DaemonService.java
index cd26b52b8..b50d6cb42 100644
--- a/ring-android/libringclient/src/main/java/net/jami/services/DaemonService.java
+++ b/ring-android/libringclient/src/main/java/net/jami/services/DaemonService.java
@@ -380,9 +380,9 @@ public class DaemonService {
 
     class DaemonDataTransferCallback extends DataTransferCallback {
         @Override
-        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);
+        public void dataTransferEvent(String accountId, String conversationId, String interactionId, String fileId, int eventCode) {
+            Log.d(TAG, "dataTransferEvent: conversationId=" + conversationId + ", fileId=" + fileId + ", eventCode=" + eventCode);
+            mAccountService.dataTransferEvent(accountId, conversationId, interactionId, fileId, eventCode);
         }
     }
 
diff --git a/ring-android/libringclient/src/main/java/net/jami/services/DeviceRuntimeService.java b/ring-android/libringclient/src/main/java/net/jami/services/DeviceRuntimeService.java
index 4f551e896..c6d5aafc5 100644
--- a/ring-android/libringclient/src/main/java/net/jami/services/DeviceRuntimeService.java
+++ b/ring-android/libringclient/src/main/java/net/jami/services/DeviceRuntimeService.java
@@ -19,6 +19,9 @@
  */
 package net.jami.services;
 
+import net.jami.model.Conversation;
+import net.jami.model.DataTransfer;
+
 import java.io.File;
 
 public abstract class DeviceRuntimeService implements DaemonService.SystemInfoCallbacks {
@@ -30,6 +33,24 @@ public abstract class DeviceRuntimeService implements DaemonService.SystemInfoCa
 
     public abstract File getFilePath(String name);
     public abstract File getConversationPath(String conversationId, String name);
+    public abstract File getConversationPath(String accountId, String conversationId, String name);
+
+    public File getConversationPath(DataTransfer interaction) {
+        return interaction.getConversationId() == null
+                ? getConversationPath(interaction.getConversation().getParticipant(), interaction.getStoragePath())
+                : interaction.getPublicPath();
+    }
+    public File getNewConversationPath(String accountId, String conversationId, String name) {
+        int prefix = 0;
+        File destPath;
+        do {
+            String fileName = prefix == 0 ? name : prefix + '_' + name;
+            destPath = getConversationPath(accountId, conversationId, fileName);
+            prefix++;
+        } while (destPath.exists());
+        return destPath;
+    }
+
     public abstract File getTemporaryPath(String conversationId, String name);
     public abstract File getConversationDir(String conversationId);
 
diff --git a/ring-android/libringclient/src/main/java/net/jami/services/NotificationService.java b/ring-android/libringclient/src/main/java/net/jami/services/NotificationService.java
index 76e5f4d60..1229695e3 100644
--- a/ring-android/libringclient/src/main/java/net/jami/services/NotificationService.java
+++ b/ring-android/libringclient/src/main/java/net/jami/services/NotificationService.java
@@ -52,7 +52,7 @@ public interface NotificationService {
     void showFileTransferNotification(Conversation conversation, DataTransfer info);
     void cancelFileNotification(int id, boolean isMigratingToService);
     void handleDataTransferNotification(DataTransfer transfer, Conversation contact, boolean remove);
-    void removeTransferNotification(String accountId, Uri conversationUri, long transferId);
+    void removeTransferNotification(String accountId, Uri conversationUri, String fileId);
     Object getDataTransferNotification(int notificationId);
 
     void updateNotification(Object notification, int notificationId);
-- 
GitLab