[android] refactor AttachmentCreator to return a single LiveData

parent cd3174a6
...@@ -14,7 +14,7 @@ import org.jsoup.UnsupportedMimeTypeException; ...@@ -14,7 +14,7 @@ import org.jsoup.UnsupportedMimeTypeException;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.util.List; import java.util.Collection;
import java.util.logging.Logger; import java.util.logging.Logger;
import static java.util.logging.Level.WARNING; import static java.util.logging.Level.WARNING;
...@@ -34,7 +34,7 @@ class AttachmentCreationTask { ...@@ -34,7 +34,7 @@ class AttachmentCreationTask {
private final MessagingManager messagingManager; private final MessagingManager messagingManager;
private final ContentResolver contentResolver; private final ContentResolver contentResolver;
private final GroupId groupId; private final GroupId groupId;
private final List<Uri> uris; private final Collection<Uri> uris;
private final boolean needsSize; private final boolean needsSize;
@Nullable @Nullable
private volatile AttachmentCreator attachmentCreator; private volatile AttachmentCreator attachmentCreator;
...@@ -44,7 +44,7 @@ class AttachmentCreationTask { ...@@ -44,7 +44,7 @@ class AttachmentCreationTask {
AttachmentCreationTask(MessagingManager messagingManager, AttachmentCreationTask(MessagingManager messagingManager,
ContentResolver contentResolver, ContentResolver contentResolver,
AttachmentCreator attachmentCreator, GroupId groupId, AttachmentCreator attachmentCreator, GroupId groupId,
List<Uri> uris, boolean needsSize) { Collection<Uri> uris, boolean needsSize) {
this.messagingManager = messagingManager; this.messagingManager = messagingManager;
this.contentResolver = contentResolver; this.contentResolver = contentResolver;
this.groupId = groupId; this.groupId = groupId;
......
...@@ -24,14 +24,14 @@ import java.io.IOException; ...@@ -24,14 +24,14 @@ import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executor; import java.util.concurrent.Executor;
import java.util.logging.Logger; import java.util.logging.Logger;
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.LogUtils.logException; import static org.briarproject.bramble.util.LogUtils.logException;
import static org.briarproject.briar.android.util.UiUtils.observeForeverOnce;
import static org.briarproject.briar.api.messaging.MessagingConstants.MAX_IMAGE_SIZE; import static org.briarproject.briar.api.messaging.MessagingConstants.MAX_IMAGE_SIZE;
@NotNullByDefault @NotNullByDefault
...@@ -45,14 +45,12 @@ public class AttachmentCreator { ...@@ -45,14 +45,12 @@ public class AttachmentCreator {
private final MessagingManager messagingManager; private final MessagingManager messagingManager;
private final AttachmentRetriever retriever; private final AttachmentRetriever retriever;
// store unsent items separately, as LiveData might not return latest value private final CopyOnWriteArrayList<Uri> uris = new CopyOnWriteArrayList<>();
private final Map<Uri, AttachmentItem> unsentItems = private final CopyOnWriteArrayList<AttachmentItemResult> itemResults =
new ConcurrentHashMap<>(); new CopyOnWriteArrayList<>();
private final Map<Uri, MutableLiveData<AttachmentItemResult>>
liveDataResult = new ConcurrentHashMap<>();
@Nullable @Nullable
private MutableLiveData<Boolean> liveDataFinished = null; private volatile MutableLiveData<AttachmentResult> result = null;
@Nullable @Nullable
private AttachmentCreationTask task; private AttachmentCreationTask task;
...@@ -65,56 +63,49 @@ public class AttachmentCreator { ...@@ -65,56 +63,49 @@ public class AttachmentCreator {
} }
@UiThread @UiThread
public AttachmentResult storeAttachments(GroupId groupId, public LiveData<AttachmentResult> storeAttachments(
Collection<Uri> uris, boolean restart) { LiveData<GroupId> groupId, Collection<Uri> newUris, boolean restart) {
List<LiveData<AttachmentItemResult>> itemResults = new ArrayList<>(); MutableLiveData<AttachmentResult> result;
if (restart) { if (restart) {
// This can happen due to configuration changes. // This can happen due to configuration changes.
// So don't create new attachments, if we have (or creating) them. // So don't create new attachments. They are already being created
// Instead, re-subscribe to the existing LiveData. // and returned by the existing LiveData.
if (task == null || isNotStoring()) throw new AssertionError(); result = this.result;
for (Uri uri : uris) { if (task == null || uris.isEmpty() || result == null)
// We don't want to expose mutable(!) LiveData throw new IllegalStateException();
LiveData<AttachmentItemResult> liveData = // A task is already running. It will update the result LiveData.
liveDataResult.get(uri); // So nothing more to do here.
if (liveData == null) throw new IllegalStateException();
itemResults.add(liveData);
}
if (liveDataFinished == null) throw new IllegalStateException();
} else { } else {
if (task != null && isNotStoring()) throw new AssertionError(); if (this.result != null || !uris.isEmpty())
List<Uri> urisToStore = new ArrayList<>(); throw new IllegalStateException();
for (Uri uri : uris) { result = new MutableLiveData<>();
urisToStore.add(uri); this.result = result;
MutableLiveData<AttachmentItemResult> liveData = uris.addAll(newUris);
new MutableLiveData<>(); observeForeverOnce(groupId, id -> {
liveDataResult.put(uri, liveData); if (id == null) throw new IllegalStateException();
itemResults.add(liveData); boolean needsSize = uris.size() == 1;
} task = new AttachmentCreationTask(messagingManager,
boolean needsSize = uris.size() == 1; app.getContentResolver(), this, id, uris, needsSize);
task = new AttachmentCreationTask(messagingManager, ioExecutor.execute(() -> task.storeAttachments());
app.getContentResolver(), this, groupId, urisToStore, });
needsSize);
ioExecutor.execute(() -> task.storeAttachments());
liveDataFinished = new MutableLiveData<>();
} }
return new AttachmentResult(itemResults, liveDataFinished); return result;
} }
@IoExecutor @IoExecutor
void onAttachmentHeaderReceived(Uri uri, AttachmentHeader h, void onAttachmentHeaderReceived(Uri uri, AttachmentHeader h,
boolean needsSize) { boolean needsSize) {
MutableLiveData<AttachmentResult> result = this.result;
if (result == null) return;
// get and cache AttachmentItem for ImagePreview // get and cache AttachmentItem for ImagePreview
try { try {
Attachment a = retriever.getMessageAttachment(h); Attachment a = retriever.getMessageAttachment(h);
AttachmentItem item = retriever.getAttachmentItem(h, a, needsSize); AttachmentItem item = retriever.getAttachmentItem(h, a, needsSize);
if (item.hasError()) throw new IOException(); if (item.hasError()) throw new IOException();
unsentItems.put(uri, item); AttachmentItemResult itemResult =
MutableLiveData<AttachmentItemResult> result = new AttachmentItemResult(uri, item);
liveDataResult.get(uri); itemResults.add(itemResult);
if (result != null) { // might have been cleared on UiThread result.postValue(new AttachmentResult(itemResults, false));
result.postValue(new AttachmentItemResult(uri, item));
}
} catch (IOException | DbException e) { } catch (IOException | DbException e) {
logException(LOG, WARNING, e); logException(LOG, WARNING, e);
onAttachmentError(uri, e); onAttachmentError(uri, e);
...@@ -123,6 +114,9 @@ public class AttachmentCreator { ...@@ -123,6 +114,9 @@ public class AttachmentCreator {
@IoExecutor @IoExecutor
void onAttachmentError(Uri uri, Throwable t) { void onAttachmentError(Uri uri, Throwable t) {
MutableLiveData<AttachmentResult> result = this.result;
if (result == null) return;
// get error message
String errorMsg; String errorMsg;
if (t instanceof UnsupportedMimeTypeException) { if (t instanceof UnsupportedMimeTypeException) {
String mimeType = ((UnsupportedMimeTypeException) t).getMimeType(); String mimeType = ((UnsupportedMimeTypeException) t).getMimeType();
...@@ -134,23 +128,29 @@ public class AttachmentCreator { ...@@ -134,23 +128,29 @@ public class AttachmentCreator {
} else { } else {
errorMsg = null; // generic error errorMsg = null; // generic error
} }
MutableLiveData<AttachmentItemResult> result = liveDataResult.get(uri); AttachmentItemResult itemResult =
if (result != null) new AttachmentItemResult(uri, errorMsg);
result.postValue(new AttachmentItemResult(errorMsg)); itemResults.add(itemResult);
result.postValue(new AttachmentResult(itemResults, false));
// expect to receive a cancel from the UI // expect to receive a cancel from the UI
} }
@IoExecutor @IoExecutor
void onAttachmentCreationFinished() { void onAttachmentCreationFinished() {
if (liveDataFinished != null) liveDataFinished.postValue(true); if (uris.size() != itemResults.size())
throw new IllegalStateException();
MutableLiveData<AttachmentResult> result = this.result;
if (result == null) return;
result.postValue(new AttachmentResult(itemResults, true));
} }
@UiThread @UiThread
public List<AttachmentHeader> getAttachmentHeadersForSending() { public List<AttachmentHeader> getAttachmentHeadersForSending() {
List<AttachmentHeader> headers = List<AttachmentHeader> headers = new ArrayList<>(itemResults.size());
new ArrayList<>(unsentItems.values().size()); for (AttachmentItemResult itemResult : itemResults) {
for (AttachmentItem item : unsentItems.values()) { // check if we are trying to send attachment items with errors
headers.add(item.getHeader()); if (itemResult.getItem() == null) throw new IllegalStateException();
headers.add(itemResult.getItem().getHeader());
} }
return headers; return headers;
} }
...@@ -161,22 +161,28 @@ public class AttachmentCreator { ...@@ -161,22 +161,28 @@ public class AttachmentCreator {
* @param id The MessageId of the sent message. * @param id The MessageId of the sent message.
*/ */
public void onAttachmentsSent(MessageId id) { public void onAttachmentsSent(MessageId id) {
retriever.cachePut(id, new ArrayList<>(unsentItems.values())); List<AttachmentItem> items = new ArrayList<>(itemResults.size());
for (AttachmentItemResult itemResult : itemResults) {
// check if we are trying to send attachment items with errors
if (itemResult.getItem() == null) throw new IllegalStateException();
items.add(itemResult.getItem());
}
retriever.cachePut(id, items);
resetState(); resetState();
} }
/**
* Needs to be called when created attachments will not be sent anymore.
*/
@UiThread @UiThread
public void cancel() { public void cancel() {
if (task == null) throw new AssertionError(); if (task == null) throw new AssertionError();
task.cancel(); task.cancel();
// let observers know that they can remove themselves // let observers know that they can remove themselves
for (MutableLiveData<AttachmentItemResult> liveData : liveDataResult MutableLiveData<AttachmentResult> result = this.result;
.values()) { if (result != null) {
if (liveData.getValue() == null) { result.setValue(null);
liveData.setValue(null);
}
} }
if (liveDataFinished != null) liveDataFinished.setValue(false);
deleteUnsentAttachments(); deleteUnsentAttachments();
resetState(); resetState();
} }
...@@ -184,20 +190,24 @@ public class AttachmentCreator { ...@@ -184,20 +190,24 @@ public class AttachmentCreator {
@UiThread @UiThread
private void resetState() { private void resetState() {
task = null; task = null;
liveDataResult.clear(); uris.clear();
liveDataFinished = null; itemResults.clear();
unsentItems.clear(); result = null;
} }
@UiThread @UiThread
public void deleteUnsentAttachments() { public void deleteUnsentAttachments() {
// Make a copy for the IoExecutor as we clear the unsentItems soon // Make a copy for the IoExecutor as we clear the itemResults soon
List<AttachmentItem> itemsToDelete = List<AttachmentHeader> headers = new ArrayList<>(itemResults.size());
new ArrayList<>(unsentItems.values()); for (AttachmentItemResult itemResult : itemResults) {
// check if we are trying to send attachment items with errors
if (itemResult.getItem() != null)
headers.add(itemResult.getItem().getHeader());
}
ioExecutor.execute(() -> { ioExecutor.execute(() -> {
for (AttachmentItem item : itemsToDelete) { for (AttachmentHeader header : headers) {
try { try {
messagingManager.removeAttachment(item.getHeader()); messagingManager.removeAttachment(header);
} catch (DbException e) { } catch (DbException e) {
logException(LOG, WARNING, e); logException(LOG, WARNING, e);
} }
...@@ -205,8 +215,4 @@ public class AttachmentCreator { ...@@ -205,8 +215,4 @@ public class AttachmentCreator {
}); });
} }
private boolean isNotStoring() {
return liveDataFinished == null;
}
} }
...@@ -11,26 +11,24 @@ import javax.annotation.concurrent.Immutable; ...@@ -11,26 +11,24 @@ import javax.annotation.concurrent.Immutable;
@NotNullByDefault @NotNullByDefault
public class AttachmentItemResult { public class AttachmentItemResult {
@Nullable
private final Uri uri; private final Uri uri;
@Nullable @Nullable
private final AttachmentItem item; private final AttachmentItem item;
@Nullable @Nullable
private final String errorMsg; private final String errorMsg;
public AttachmentItemResult(Uri uri, AttachmentItem item) { AttachmentItemResult(Uri uri, AttachmentItem item) {
this.uri = uri; this.uri = uri;
this.item = item; this.item = item;
this.errorMsg = null; this.errorMsg = null;
} }
public AttachmentItemResult(@Nullable String errorMsg) { AttachmentItemResult(Uri uri, @Nullable String errorMsg) {
this.uri = null; this.uri = uri;
this.item = null; this.item = null;
this.errorMsg = errorMsg; this.errorMsg = errorMsg;
} }
@Nullable
public Uri getUri() { public Uri getUri() {
return uri; return uri;
} }
...@@ -40,8 +38,8 @@ public class AttachmentItemResult { ...@@ -40,8 +38,8 @@ public class AttachmentItemResult {
return item; return item;
} }
public boolean isError() { public boolean hasError() {
return errorMsg != null; return item == null;
} }
@Nullable @Nullable
......
package org.briarproject.briar.android.attachment; package org.briarproject.briar.android.attachment;
import android.arch.lifecycle.LiveData;
import android.net.Uri; import android.net.Uri;
import android.support.annotation.UiThread; import android.support.annotation.UiThread;
...@@ -11,7 +12,8 @@ import java.util.List; ...@@ -11,7 +12,8 @@ import java.util.List;
@UiThread @UiThread
public interface AttachmentManager { public interface AttachmentManager {
AttachmentResult storeAttachments(Collection<Uri> uri, boolean restart); LiveData<AttachmentResult> storeAttachments(Collection<Uri> uri,
boolean restart);
List<AttachmentHeader> getAttachmentHeadersForSending(); List<AttachmentHeader> getAttachmentHeadersForSending();
......
package org.briarproject.briar.android.attachment; package org.briarproject.briar.android.attachment;
import android.arch.lifecycle.LiveData;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import java.util.Collection; import java.util.Collection;
...@@ -12,21 +10,20 @@ import javax.annotation.concurrent.Immutable; ...@@ -12,21 +10,20 @@ import javax.annotation.concurrent.Immutable;
@NotNullByDefault @NotNullByDefault
public class AttachmentResult { public class AttachmentResult {
private final Collection<LiveData<AttachmentItemResult>> itemResults; private final Collection<AttachmentItemResult> itemResults;
private final LiveData<Boolean> finished; private final boolean finished;
public AttachmentResult( public AttachmentResult(Collection<AttachmentItemResult> itemResults,
Collection<LiveData<AttachmentItemResult>> itemResults, boolean finished) {
LiveData<Boolean> finished) {
this.itemResults = itemResults; this.itemResults = itemResults;
this.finished = finished; this.finished = finished;
} }
public Collection<LiveData<AttachmentItemResult>> getItemResults() { public Collection<AttachmentItemResult> getItemResults() {
return itemResults; return itemResults;
} }
public LiveData<Boolean> getFinished() { public boolean isFinished() {
return finished; return finished;
} }
......
...@@ -54,6 +54,7 @@ import static org.briarproject.bramble.util.LogUtils.logException; ...@@ -54,6 +54,7 @@ import static org.briarproject.bramble.util.LogUtils.logException;
import static org.briarproject.bramble.util.LogUtils.now; import static org.briarproject.bramble.util.LogUtils.now;
import static org.briarproject.briar.android.attachment.AttachmentDimensions.getAttachmentDimensions; import static org.briarproject.briar.android.attachment.AttachmentDimensions.getAttachmentDimensions;
import static org.briarproject.briar.android.settings.SettingsFragment.SETTINGS_NAMESPACE; import static org.briarproject.briar.android.settings.SettingsFragment.SETTINGS_NAMESPACE;
import static org.briarproject.briar.android.util.UiUtils.observeForeverOnce;
@NotNullByDefault @NotNullByDefault
public class ConversationViewModel extends AndroidViewModel public class ConversationViewModel extends AndroidViewModel
...@@ -80,13 +81,12 @@ public class ConversationViewModel extends AndroidViewModel ...@@ -80,13 +81,12 @@ public class ConversationViewModel extends AndroidViewModel
@Nullable @Nullable
private ContactId contactId = null; private ContactId contactId = null;
@Nullable
private volatile GroupId messagingGroupId = null;
private final MutableLiveData<Contact> contact = new MutableLiveData<>(); private final MutableLiveData<Contact> contact = new MutableLiveData<>();
private final LiveData<AuthorId> contactAuthorId = private final LiveData<AuthorId> contactAuthorId =
Transformations.map(contact, c -> c.getAuthor().getId()); Transformations.map(contact, c -> c.getAuthor().getId());
private final LiveData<String> contactName = private final LiveData<String> contactName =
Transformations.map(contact, UiUtils::getContactDisplayName); Transformations.map(contact, UiUtils::getContactDisplayName);
private final LiveData<GroupId> messagingGroupId;
private final MutableLiveData<Boolean> imageSupport = private final MutableLiveData<Boolean> imageSupport =
new MutableLiveData<>(); new MutableLiveData<>();
private final MutableLiveEvent<Boolean> showImageOnboarding = private final MutableLiveEvent<Boolean> showImageOnboarding =
...@@ -120,6 +120,8 @@ public class ConversationViewModel extends AndroidViewModel ...@@ -120,6 +120,8 @@ public class ConversationViewModel extends AndroidViewModel
getAttachmentDimensions(application.getResources())); getAttachmentDimensions(application.getResources()));
this.attachmentCreator = new AttachmentCreator(getApplication(), this.attachmentCreator = new AttachmentCreator(getApplication(),
ioExecutor, messagingManager, attachmentRetriever); ioExecutor, messagingManager, attachmentRetriever);
messagingGroupId = Transformations
.map(contact, c -> messagingManager.getContactGroup(c).getId());
contactDeleted.setValue(false); contactDeleted.setValue(false);
} }
...@@ -150,9 +152,6 @@ public class ConversationViewModel extends AndroidViewModel ...@@ -150,9 +152,6 @@ public class ConversationViewModel extends AndroidViewModel
contact.postValue(c); contact.postValue(c);
logDuration(LOG, "Loading contact", start); logDuration(LOG, "Loading contact", start);
start = now(); start = now();
messagingGroupId = messagingManager.getContactGroup(c).getId();
logDuration(LOG, "Load conversation GroupId", start);
start = now();
checkFeaturesAndOnboarding(contactId); checkFeaturesAndOnboarding(contactId);
logDuration(LOG, "Checking for image support", start); logDuration(LOG, "Checking for image support", start);
} catch (NoSuchContactException e) { } catch (NoSuchContactException e) {
...@@ -189,18 +188,20 @@ public class ConversationViewModel extends AndroidViewModel ...@@ -189,18 +188,20 @@ public class ConversationViewModel extends AndroidViewModel
void sendMessage(@Nullable String text, void sendMessage(@Nullable String text,
List<AttachmentHeader> attachmentHeaders, long timestamp) { List<AttachmentHeader> attachmentHeaders, long timestamp) {
GroupId groupId = messagingGroupId; // messagingGroupId is loaded with the contact
if (groupId == null) throw new IllegalStateException(); observeForeverOnce(messagingGroupId, groupId -> {
createMessage(groupId, text, attachmentHeaders, timestamp); if (groupId == null) throw new IllegalStateException();
createMessage(groupId, text, attachmentHeaders, timestamp);
});
} }
@Override @Override
@UiThread @UiThread
public AttachmentResult storeAttachments(Collection<Uri> uris, public LiveData<AttachmentResult> storeAttachments(Collection<Uri> uris,
boolean restart) { boolean restart) {
GroupId groupId = messagingGroupId; // messagingGroupId is loaded with the contact
if (groupId == null) throw new IllegalStateException(); return attachmentCreator
return attachmentCreator.storeAttachments(groupId, uris, restart); .storeAttachments(messagingGroupId, uris, restart);
} }
@Override @Override
......
...@@ -15,6 +15,7 @@ import java.util.Collection; ...@@ -15,6 +15,7 @@ import java.util.Collection;
import static android.content.Context.LAYOUT_INFLATER_SERVICE; import static android.content.Context.LAYOUT_INFLATER_SERVICE;
import static android.support.v4.content.ContextCompat.getColor; import static android.support.v4.content.ContextCompat.getColor;
import static android.support.v7.widget.RecyclerView.NO_POSITION;
import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
import static java.util.Objects.requireNonNull; import static java.util.Objects.requireNonNull;
...@@ -75,7 +76,10 @@ public class ImagePreview extends ConstraintLayout { ...@@ -75,7 +76,10 @@ public class ImagePreview extends ConstraintLayout {
void loadPreviewImage(AttachmentItemResult result) { void loadPreviewImage(AttachmentItemResult result) {
ImagePreviewAdapter adapter = ImagePreviewAdapter adapter =
((ImagePreviewAdapter) imageList.getAdapter()); ((ImagePreviewAdapter) imageList.getAdapter());
requireNonNull(adapter).loadItemPreview(result); int pos = requireNonNull(adapter).loadItemPreview(result);
if (pos != NO_POSITION) {
imageList.smoothScrollToPosition(pos);
}
} }
interface ImagePreviewListener { interface ImagePreviewListener {
...