[android] Create attachments before showing previews

parent 249e1e28
package org.briarproject.briar.android.blog; package org.briarproject.briar.android.blog;
import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
...@@ -21,6 +20,7 @@ import org.briarproject.briar.android.fragment.BaseFragment; ...@@ -21,6 +20,7 @@ import org.briarproject.briar.android.fragment.BaseFragment;
import org.briarproject.briar.android.view.TextInputView; import org.briarproject.briar.android.view.TextInputView;
import org.briarproject.briar.android.view.TextSendController; import org.briarproject.briar.android.view.TextSendController;
import org.briarproject.briar.android.view.TextSendController.SendListener; import org.briarproject.briar.android.view.TextSendController.SendListener;
import org.briarproject.briar.api.messaging.AttachmentHeader;
import java.util.List; import java.util.List;
...@@ -121,7 +121,8 @@ public class ReblogFragment extends BaseFragment implements SendListener { ...@@ -121,7 +121,8 @@ public class ReblogFragment extends BaseFragment implements SendListener {
} }
@Override @Override
public void onSendClick(@Nullable String text, List<Uri> imageUris) { public void onSendClick(@Nullable String text,
List<AttachmentHeader> headers) {
ui.input.hideSoftKeyboard(); ui.input.hideSoftKeyboard();
feedController.repeatPost(item, text, feedController.repeatPost(item, text,
new UiExceptionHandler<DbException>(this) { new UiExceptionHandler<DbException>(this) {
......
package org.briarproject.briar.android.blog; package org.briarproject.briar.android.blog;
import android.content.Intent; import android.content.Intent;
import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.view.KeyEvent; import android.view.KeyEvent;
...@@ -27,6 +26,7 @@ import org.briarproject.briar.api.android.AndroidNotificationManager; ...@@ -27,6 +26,7 @@ import org.briarproject.briar.api.android.AndroidNotificationManager;
import org.briarproject.briar.api.blog.BlogManager; import org.briarproject.briar.api.blog.BlogManager;
import org.briarproject.briar.api.blog.BlogPost; import org.briarproject.briar.api.blog.BlogPost;
import org.briarproject.briar.api.blog.BlogPostFactory; import org.briarproject.briar.api.blog.BlogPostFactory;
import org.briarproject.briar.api.messaging.AttachmentHeader;
import java.security.GeneralSecurityException; import java.security.GeneralSecurityException;
import java.util.List; import java.util.List;
...@@ -120,7 +120,8 @@ public class WriteBlogPostActivity extends BriarActivity ...@@ -120,7 +120,8 @@ public class WriteBlogPostActivity extends BriarActivity
} }
@Override @Override
public void onSendClick(@Nullable String text, List<Uri> imageUris) { public void onSendClick(@Nullable String text,
List<AttachmentHeader> headers) {
if (isNullOrEmpty(text)) throw new AssertionError(); if (isNullOrEmpty(text)) throw new AssertionError();
// hide publish button, show progress bar // hide publish button, show progress bar
......
package org.briarproject.briar.android.conversation; package org.briarproject.briar.android.conversation;
import android.content.ContentResolver;
import android.graphics.BitmapFactory; import android.graphics.BitmapFactory;
import android.graphics.BitmapFactory.Options; import android.graphics.BitmapFactory.Options;
import android.net.Uri;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.support.annotation.VisibleForTesting;
import android.support.media.ExifInterface; import android.support.media.ExifInterface;
import android.webkit.MimeTypeMap; import android.webkit.MimeTypeMap;
...@@ -12,6 +15,7 @@ import org.briarproject.bramble.api.Pair; ...@@ -12,6 +15,7 @@ import org.briarproject.bramble.api.Pair;
import org.briarproject.bramble.api.db.DatabaseExecutor; import org.briarproject.bramble.api.db.DatabaseExecutor;
import org.briarproject.bramble.api.db.DbException; import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.sync.GroupId;
import org.briarproject.bramble.api.sync.MessageId; import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.briar.android.conversation.ImageHelper.DecodeResult; import org.briarproject.briar.android.conversation.ImageHelper.DecodeResult;
import org.briarproject.briar.api.messaging.Attachment; import org.briarproject.briar.api.messaging.Attachment;
...@@ -25,6 +29,7 @@ import java.util.ArrayList; ...@@ -25,6 +29,7 @@ import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.logging.Logger; import java.util.logging.Logger;
import static android.support.media.ExifInterface.ORIENTATION_ROTATE_270; import static android.support.media.ExifInterface.ORIENTATION_ROTATE_270;
...@@ -54,6 +59,7 @@ class AttachmentController { ...@@ -54,6 +59,7 @@ class AttachmentController {
private final int minWidth, maxWidth; private final int minWidth, maxWidth;
private final int minHeight, maxHeight; private final int minHeight, maxHeight;
private final List<AttachmentHeader> unsent = new CopyOnWriteArrayList<>();
private final Map<MessageId, List<AttachmentItem>> attachmentCache = private final Map<MessageId, List<AttachmentItem>> attachmentCache =
new ConcurrentHashMap<>(); new ConcurrentHashMap<>();
...@@ -114,6 +120,54 @@ class AttachmentController { ...@@ -114,6 +120,54 @@ class AttachmentController {
return attachments; return attachments;
} }
@DatabaseExecutor
AttachmentHeader createAttachmentHeader(ContentResolver contentResolver,
GroupId groupId, Uri uri)
throws IOException, DbException {
InputStream is = contentResolver.openInputStream(uri);
if (is == null) throw new IOException();
String contentType = contentResolver.getType(uri);
if (contentType == null) throw new IOException("null content type");
long timestamp = System.currentTimeMillis();
AttachmentHeader h = messagingManager
.addLocalAttachment(groupId, timestamp, contentType, is);
tryToClose(is, LOG, WARNING);
unsent.add(h);
return h;
}
@DatabaseExecutor
void deleteUnsentAttachments() {
for (AttachmentHeader h : unsent) {
try {
messagingManager.removeAttachment(h);
} catch (DbException e) {
logException(LOG, WARNING, e);
}
}
}
List<AttachmentHeader> getUnsentAttachments() {
return new ArrayList<>(unsent);
}
void markAttachmentsSent() {
unsent.clear();
}
@DatabaseExecutor
AttachmentItem getAttachmentItem(ContentResolver contentResolver, Uri uri,
AttachmentHeader h, boolean needsSize) throws IOException {
InputStream is = null;
try {
is = contentResolver.openInputStream(uri);
if (is == null) throw new IOException();
return getAttachmentItem(h, new Attachment(is), needsSize);
} finally {
if (is != null) tryToClose(is, LOG, WARNING);
}
}
/** /**
* Creates {@link AttachmentItem}s from the passed headers and Attachments. * Creates {@link AttachmentItem}s from the passed headers and Attachments.
* <p> * <p>
...@@ -135,6 +189,7 @@ class AttachmentController { ...@@ -135,6 +189,7 @@ class AttachmentController {
* Creates an {@link AttachmentItem} from the {@link Attachment}'s * Creates an {@link AttachmentItem} from the {@link Attachment}'s
* {@link InputStream} which will be closed when this method returns. * {@link InputStream} which will be closed when this method returns.
*/ */
@VisibleForTesting
AttachmentItem getAttachmentItem(AttachmentHeader h, Attachment a, AttachmentItem getAttachmentItem(AttachmentHeader h, Attachment a,
boolean needsSize) { boolean needsSize) {
MessageId messageId = h.getMessageId(); MessageId messageId = h.getMessageId();
......
...@@ -6,7 +6,6 @@ import android.arch.lifecycle.ViewModelProvider; ...@@ -6,7 +6,6 @@ import android.arch.lifecycle.ViewModelProvider;
import android.arch.lifecycle.ViewModelProviders; import android.arch.lifecycle.ViewModelProviders;
import android.content.DialogInterface; import android.content.DialogInterface;
import android.content.Intent; import android.content.Intent;
import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.os.Parcelable; import android.os.Parcelable;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
...@@ -264,7 +263,7 @@ public class ConversationActivity extends BriarActivity ...@@ -264,7 +263,7 @@ public class ConversationActivity extends BriarActivity
if (FEATURE_FLAG_IMAGE_ATTACHMENTS) { if (FEATURE_FLAG_IMAGE_ATTACHMENTS) {
ImagePreview imagePreview = findViewById(R.id.imagePreview); ImagePreview imagePreview = findViewById(R.id.imagePreview);
sendController = new TextAttachmentController(textInputView, sendController = new TextAttachmentController(textInputView,
imagePreview, this, this); imagePreview, this, this, viewModel);
observeOnce(viewModel.hasImageSupport(), this, hasSupport -> { observeOnce(viewModel.hasImageSupport(), this, hasSupport -> {
if (hasSupport != null && hasSupport) { if (hasSupport != null && hasSupport) {
// remove cast when removing FEATURE_FLAG_IMAGE_ATTACHMENTS // remove cast when removing FEATURE_FLAG_IMAGE_ATTACHMENTS
...@@ -658,12 +657,13 @@ public class ConversationActivity extends BriarActivity ...@@ -658,12 +657,13 @@ public class ConversationActivity extends BriarActivity
} }
@Override @Override
public void onSendClick(@Nullable String text, List<Uri> imageUris) { public void onSendClick(@Nullable String text,
if (isNullOrEmpty(text) && imageUris.isEmpty()) List<AttachmentHeader> attachmentHeaders) {
if (isNullOrEmpty(text) && attachmentHeaders.isEmpty())
throw new AssertionError(); throw new AssertionError();
long timestamp = System.currentTimeMillis(); long timestamp = System.currentTimeMillis();
timestamp = Math.max(timestamp, getMinTimestampForNewMessage()); timestamp = Math.max(timestamp, getMinTimestampForNewMessage());
viewModel.sendMessage(text, imageUris, timestamp); viewModel.sendMessage(text, attachmentHeaders, timestamp);
textInputView.clearText(); textInputView.clearText();
} }
......
...@@ -11,7 +11,6 @@ import android.support.annotation.Nullable; ...@@ -11,7 +11,6 @@ import android.support.annotation.Nullable;
import android.support.annotation.UiThread; import android.support.annotation.UiThread;
import org.briarproject.bramble.api.FormatException; import org.briarproject.bramble.api.FormatException;
import org.briarproject.bramble.api.Pair;
import org.briarproject.bramble.api.contact.Contact; import org.briarproject.bramble.api.contact.Contact;
import org.briarproject.bramble.api.contact.ContactId; import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.contact.ContactManager; import org.briarproject.bramble.api.contact.ContactManager;
...@@ -27,10 +26,11 @@ import org.briarproject.bramble.api.settings.SettingsManager; ...@@ -27,10 +26,11 @@ import org.briarproject.bramble.api.settings.SettingsManager;
import org.briarproject.bramble.api.sync.GroupId; import org.briarproject.bramble.api.sync.GroupId;
import org.briarproject.bramble.api.sync.Message; import org.briarproject.bramble.api.sync.Message;
import org.briarproject.bramble.api.sync.MessageId; import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.bramble.api.system.AndroidExecutor;
import org.briarproject.briar.android.util.UiUtils; import org.briarproject.briar.android.util.UiUtils;
import org.briarproject.briar.android.view.TextAttachmentController.AttachmentManager;
import org.briarproject.briar.android.viewmodel.LiveEvent; import org.briarproject.briar.android.viewmodel.LiveEvent;
import org.briarproject.briar.android.viewmodel.MutableLiveEvent; import org.briarproject.briar.android.viewmodel.MutableLiveEvent;
import org.briarproject.briar.api.messaging.Attachment;
import org.briarproject.briar.api.messaging.AttachmentHeader; import org.briarproject.briar.api.messaging.AttachmentHeader;
import org.briarproject.briar.api.messaging.MessagingManager; import org.briarproject.briar.api.messaging.MessagingManager;
import org.briarproject.briar.api.messaging.PrivateMessage; import org.briarproject.briar.api.messaging.PrivateMessage;
...@@ -38,10 +38,11 @@ import org.briarproject.briar.api.messaging.PrivateMessageFactory; ...@@ -38,10 +38,11 @@ import org.briarproject.briar.api.messaging.PrivateMessageFactory;
import org.briarproject.briar.api.messaging.PrivateMessageHeader; import org.briarproject.briar.api.messaging.PrivateMessageHeader;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.concurrent.Executor; import java.util.concurrent.Executor;
import java.util.logging.Logger; import java.util.logging.Logger;
...@@ -50,7 +51,6 @@ import javax.inject.Inject; ...@@ -50,7 +51,6 @@ import javax.inject.Inject;
import static java.util.Objects.requireNonNull; import static java.util.Objects.requireNonNull;
import static java.util.logging.Level.WARNING; import static java.util.logging.Level.WARNING;
import static java.util.logging.Logger.getLogger; import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.util.IoUtils.tryToClose;
import static org.briarproject.bramble.util.LogUtils.logDuration; import static org.briarproject.bramble.util.LogUtils.logDuration;
import static org.briarproject.bramble.util.LogUtils.logException; import static org.briarproject.bramble.util.LogUtils.logException;
import static org.briarproject.bramble.util.LogUtils.now; import static org.briarproject.bramble.util.LogUtils.now;
...@@ -59,7 +59,8 @@ import static org.briarproject.briar.android.settings.SettingsFragment.SETTINGS_ ...@@ -59,7 +59,8 @@ import static org.briarproject.briar.android.settings.SettingsFragment.SETTINGS_
import static org.briarproject.briar.android.util.UiUtils.observeForeverOnce; import static org.briarproject.briar.android.util.UiUtils.observeForeverOnce;
@NotNullByDefault @NotNullByDefault
public class ConversationViewModel extends AndroidViewModel { public class ConversationViewModel extends AndroidViewModel implements
AttachmentManager {
private static Logger LOG = private static Logger LOG =
getLogger(ConversationViewModel.class.getName()); getLogger(ConversationViewModel.class.getName());
...@@ -73,6 +74,7 @@ public class ConversationViewModel extends AndroidViewModel { ...@@ -73,6 +74,7 @@ public class ConversationViewModel extends AndroidViewModel {
@CryptoExecutor @CryptoExecutor
private final Executor cryptoExecutor; private final Executor cryptoExecutor;
private final TransactionManager db; private final TransactionManager db;
private final AndroidExecutor androidExecutor;
private final MessagingManager messagingManager; private final MessagingManager messagingManager;
private final ContactManager contactManager; private final ContactManager contactManager;
private final SettingsManager settingsManager; private final SettingsManager settingsManager;
...@@ -100,17 +102,20 @@ public class ConversationViewModel extends AndroidViewModel { ...@@ -100,17 +102,20 @@ public class ConversationViewModel extends AndroidViewModel {
new MutableLiveData<>(); new MutableLiveData<>();
private final MutableLiveData<PrivateMessageHeader> addedHeader = private final MutableLiveData<PrivateMessageHeader> addedHeader =
new MutableLiveData<>(); new MutableLiveData<>();
// TODO move to AttachmentController
private final Map<Uri, AttachmentItem> unsentItems = new HashMap<>();
@Inject @Inject
ConversationViewModel(Application application, ConversationViewModel(Application application,
@DatabaseExecutor Executor dbExecutor, @DatabaseExecutor Executor dbExecutor,
@CryptoExecutor Executor cryptoExecutor, TransactionManager db, @CryptoExecutor Executor cryptoExecutor, TransactionManager db,
MessagingManager messagingManager, ContactManager contactManager, AndroidExecutor androidExecutor, MessagingManager messagingManager,
SettingsManager settingsManager, ContactManager contactManager, SettingsManager settingsManager,
PrivateMessageFactory privateMessageFactory) { PrivateMessageFactory privateMessageFactory) {
super(application); super(application);
this.dbExecutor = dbExecutor; this.dbExecutor = dbExecutor;
this.cryptoExecutor = cryptoExecutor; this.cryptoExecutor = cryptoExecutor;
this.androidExecutor = androidExecutor;
this.db = db; this.db = db;
this.messagingManager = messagingManager; this.messagingManager = messagingManager;
this.contactManager = contactManager; this.contactManager = contactManager;
...@@ -121,6 +126,12 @@ public class ConversationViewModel extends AndroidViewModel { ...@@ -121,6 +126,12 @@ public class ConversationViewModel extends AndroidViewModel {
contactDeleted.setValue(false); contactDeleted.setValue(false);
} }
@Override
protected void onCleared() {
super.onCleared();
attachmentController.deleteUnsentAttachments();
}
/** /**
* Setting the {@link ContactId} automatically triggers loading of other * Setting the {@link ContactId} automatically triggers loading of other
* data. * data.
...@@ -176,15 +187,56 @@ public class ConversationViewModel extends AndroidViewModel { ...@@ -176,15 +187,56 @@ public class ConversationViewModel extends AndroidViewModel {
}); });
} }
void sendMessage(@Nullable String text, List<Uri> uris, long timestamp) { void sendMessage(@Nullable String text,
List<AttachmentHeader> attachmentHeaders, long timestamp) {
if (messagingGroupId.getValue() == null) loadGroupId(); if (messagingGroupId.getValue() == null) loadGroupId();
observeForeverOnce(messagingGroupId, groupId -> { observeForeverOnce(messagingGroupId, groupId -> {
if (groupId == null) return; if (groupId == null) return;
// calls through to creating and storing the message createMessage(groupId, text, attachmentHeaders, timestamp);
storeAttachments(groupId, text, uris, timestamp);
}); });
} }
@Override
public void storeAttachment(Uri uri, boolean needsSize, Runnable onSuccess,
Runnable onError) {
if (unsentItems.containsKey(uri)) {
// This can happen due to configuration (screen orientation) change.
// So don't create a new attachment, if we have one already.
androidExecutor.runOnUiThread(onSuccess);
return;
}
if (messagingGroupId.getValue() == null) loadGroupId();
observeForeverOnce(messagingGroupId, groupId -> dbExecutor.execute(()
-> {
if (groupId == null) throw new IllegalStateException();
long start = now();
try {
ContentResolver contentResolver =
getApplication().getContentResolver();
AttachmentHeader h = attachmentController
.createAttachmentHeader(contentResolver, groupId, uri);
unsentItems.put(uri, attachmentController
.getAttachmentItem(contentResolver, uri, h, needsSize));
androidExecutor.runOnUiThread(onSuccess);
} catch (DbException | IOException e) {
logException(LOG, WARNING, e);
androidExecutor.runOnUiThread(onError);
}
logDuration(LOG, "Storing attachment", start);
}));
}
@Override
public List<AttachmentHeader> getAttachments() {
return attachmentController.getUnsentAttachments();
}
@Override
public void removeAttachments() {
unsentItems.clear();
dbExecutor.execute(attachmentController::deleteUnsentAttachments);
}
private void loadGroupId() { private void loadGroupId() {
if (contactId == null) throw new IllegalStateException(); if (contactId == null) throw new IllegalStateException();
dbExecutor.execute(() -> { dbExecutor.execute(() -> {
...@@ -252,58 +304,8 @@ public class ConversationViewModel extends AndroidViewModel { ...@@ -252,58 +304,8 @@ public class ConversationViewModel extends AndroidViewModel {
}); });
} }
private void storeAttachments(GroupId groupId, @Nullable String text,
List<Uri> uris, long timestamp) {
dbExecutor.execute(() -> {
long start = now();
List<AttachmentHeader> attachments = new ArrayList<>();
List<AttachmentItem> items = new ArrayList<>();
boolean needsSize = uris.size() == 1;
for (Uri uri : uris) {
Pair<AttachmentHeader, AttachmentItem> pair =
createAttachmentHeader(groupId, uri, timestamp,
needsSize);
if (pair == null) continue;
attachments.add(pair.getFirst());
items.add(pair.getSecond());
}
logDuration(LOG, "Storing attachments", start);
createMessage(groupId, text, attachments, items, timestamp);
});
}
@Nullable
@DatabaseExecutor
private Pair<AttachmentHeader, AttachmentItem> createAttachmentHeader(
GroupId groupId, Uri uri, long timestamp, boolean needsSize) {
InputStream is = null;
try {
ContentResolver contentResolver =
getApplication().getContentResolver();
is = contentResolver.openInputStream(uri);
if (is == null) throw new IOException();
String contentType = contentResolver.getType(uri);
if (contentType == null) throw new IOException("null content type");
AttachmentHeader h = messagingManager
.addLocalAttachment(groupId, timestamp, contentType, is);
is.close();
// re-open stream to get AttachmentItem
is = contentResolver.openInputStream(uri);
if (is == null) throw new IOException();
AttachmentItem item = attachmentController
.getAttachmentItem(h, new Attachment(is), needsSize);
return new Pair<>(h, item);
} catch (DbException | IOException e) {
logException(LOG, WARNING, e);
return null;
} finally {
if (is != null) tryToClose(is, LOG, WARNING);
}
}
private void createMessage(GroupId groupId, @Nullable String text, private void createMessage(GroupId groupId, @Nullable String text,
List<AttachmentHeader> attachments, List<AttachmentItem> aItems, List<AttachmentHeader> attachments, long timestamp) {
long timestamp) {
cryptoExecutor.execute(() -> { cryptoExecutor.execute(() -> {
try { try {
// TODO remove when text can be null in the backend // TODO remove when text can be null in the backend
...@@ -311,7 +313,6 @@ public class ConversationViewModel extends AndroidViewModel { ...@@ -311,7 +313,6 @@ public class ConversationViewModel extends AndroidViewModel {
PrivateMessage pm = privateMessageFactory PrivateMessage pm = privateMessageFactory
.createPrivateMessage(groupId, timestamp, msgText, .createPrivateMessage(groupId, timestamp, msgText,
attachments); attachments);
attachmentController.put(pm.getMessage().getId(), aItems);
storeMessage(pm, msgText, attachments); storeMessage(pm, msgText, attachments);
} catch (FormatException e) { } catch (FormatException e) {
throw new RuntimeException(e); throw new RuntimeException(e);
...@@ -331,6 +332,10 @@ public class ConversationViewModel extends AndroidViewModel { ...@@ -331,6 +332,10 @@ public class ConversationViewModel extends AndroidViewModel {
message.getId(), message.getGroupId(), message.getId(), message.getGroupId(),
message.getTimestamp(), true, true, false, false, message.getTimestamp(), true, true, false, false,
text != null, attachments); text != null, attachments);
attachmentController.put(m.getMessage().getId(),
new ArrayList<>(unsentItems.values()));
unsentItems.clear();
attachmentController.markAttachmentsSent();
// TODO add text to cache when available here // TODO add text to cache when available here
addedHeader.postValue(h); addedHeader.postValue(h);
} catch (DbException e) { } catch (DbException e) {
......
package org.briarproject.briar.android.introduction; package org.briarproject.briar.android.introduction;
import android.content.Context; import android.content.Context;
import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.support.v7.app.ActionBar; import android.support.v7.app.ActionBar;
...@@ -26,6 +25,7 @@ import org.briarproject.briar.android.view.TextInputView; ...@@ -26,6 +25,7 @@ import org.briarproject.briar.android.view.TextInputView;
import org.briarproject.briar.android.view.TextSendController; import org.briarproject.briar.android.view.TextSendController;
import org.briarproject.briar.android.view.TextSendController.SendListener; import org.briarproject.briar.android.view.TextSendController.SendListener;
import org.briarproject.briar.api.introduction.IntroductionManager; import org.briarproject.briar.api.introduction.IntroductionManager;
import org.briarproject.briar.api.messaging.AttachmentHeader;