mirror of
https://code.briarproject.org/briar/briar.git
synced 2026-02-12 10:49:06 +01:00
[android] Refactor attachment loading to use LiveData
This commit is contained in:
@@ -28,7 +28,7 @@ public class AttachmentRetrieverIntegrationTest {
|
||||
|
||||
private final ImageHelper imageHelper = new ImageHelperImpl();
|
||||
private final AttachmentRetriever retriever =
|
||||
new AttachmentRetrieverImpl(null, dimensions, imageHelper,
|
||||
new AttachmentRetrieverImpl(null, null, dimensions, imageHelper,
|
||||
new ImageSizeCalculator(imageHelper));
|
||||
|
||||
@Test
|
||||
|
||||
@@ -21,7 +21,13 @@ import static org.briarproject.briar.android.attachment.AttachmentItem.State.MIS
|
||||
@NotNullByDefault
|
||||
public class AttachmentItem implements Parcelable {
|
||||
|
||||
public enum State {LOADING, MISSING, AVAILABLE, ERROR}
|
||||
public enum State {
|
||||
LOADING, MISSING, AVAILABLE, ERROR;
|
||||
|
||||
public boolean isFinal() {
|
||||
return this == AVAILABLE || this == ERROR;
|
||||
}
|
||||
}
|
||||
|
||||
private final AttachmentHeader header;
|
||||
private final int width, height;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package org.briarproject.briar.android.attachment;
|
||||
|
||||
import org.briarproject.bramble.api.Pair;
|
||||
import org.briarproject.bramble.api.db.DatabaseExecutor;
|
||||
import org.briarproject.bramble.api.db.DbException;
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
@@ -8,11 +7,12 @@ import org.briarproject.bramble.api.sync.MessageId;
|
||||
import org.briarproject.briar.api.messaging.Attachment;
|
||||
import org.briarproject.briar.api.messaging.AttachmentHeader;
|
||||
import org.briarproject.briar.api.messaging.PrivateMessageHeader;
|
||||
import org.briarproject.briar.api.messaging.event.AttachmentReceivedEvent;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.util.List;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.lifecycle.LiveData;
|
||||
|
||||
|
||||
@NotNullByDefault
|
||||
@@ -21,10 +21,19 @@ public interface AttachmentRetriever {
|
||||
@DatabaseExecutor
|
||||
Attachment getMessageAttachment(AttachmentHeader h) throws DbException;
|
||||
|
||||
List<AttachmentItem> getAttachmentItems(PrivateMessageHeader messageHeader);
|
||||
/**
|
||||
* Returns a list of observable {@link LiveData}
|
||||
* that get updated as the state of their {@link AttachmentItem}s changes.
|
||||
*/
|
||||
List<LiveData<AttachmentItem>> getAttachmentItems(
|
||||
PrivateMessageHeader messageHeader);
|
||||
|
||||
/**
|
||||
* Retrieves item size and adds the item to the cache, if available.
|
||||
* <p>
|
||||
* Use this to eagerly load the attachment size before it gets displayed.
|
||||
* This is needed for messages containing a single attachment.
|
||||
* Messages with more than one attachment use a standard size.
|
||||
*/
|
||||
@DatabaseExecutor
|
||||
void cacheAttachmentItemWithSize(MessageId conversationMessageId,
|
||||
@@ -37,15 +46,11 @@ public interface AttachmentRetriever {
|
||||
AttachmentItem createAttachmentItem(Attachment a, boolean needsSize);
|
||||
|
||||
/**
|
||||
* Load an {@link AttachmentItem} from the database.
|
||||
*
|
||||
* @return a pair of the {@link MessageId} of the conversation message
|
||||
* and the {@link AttachmentItem}
|
||||
* or {@code null} when the private message did not yet arrive.
|
||||
* Loads an {@link AttachmentItem}
|
||||
* that arrived via an {@link AttachmentReceivedEvent}
|
||||
* and notifies the associated {@link LiveData}.
|
||||
*/
|
||||
@Nullable
|
||||
@DatabaseExecutor
|
||||
Pair<MessageId, AttachmentItem> loadAttachmentItem(MessageId attachmentId)
|
||||
throws DbException;
|
||||
void loadAttachmentItem(MessageId attachmentId);
|
||||
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package org.briarproject.briar.android.attachment;
|
||||
|
||||
import org.briarproject.bramble.api.Pair;
|
||||
import org.briarproject.bramble.api.db.DatabaseExecutor;
|
||||
import org.briarproject.bramble.api.db.DbException;
|
||||
import org.briarproject.bramble.api.db.NoSuchMessageException;
|
||||
@@ -18,15 +17,19 @@ import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
|
||||
import static java.util.Objects.requireNonNull;
|
||||
import static java.util.logging.Level.WARNING;
|
||||
import static java.util.logging.Logger.getLogger;
|
||||
import static org.briarproject.bramble.util.IoUtils.tryToClose;
|
||||
import static org.briarproject.bramble.util.LogUtils.logException;
|
||||
import static org.briarproject.briar.android.attachment.AttachmentItem.State.AVAILABLE;
|
||||
import static org.briarproject.briar.android.attachment.AttachmentItem.State.ERROR;
|
||||
import static org.briarproject.briar.android.attachment.AttachmentItem.State.LOADING;
|
||||
@@ -38,6 +41,8 @@ class AttachmentRetrieverImpl implements AttachmentRetriever {
|
||||
private static final Logger LOG =
|
||||
getLogger(AttachmentRetrieverImpl.class.getName());
|
||||
|
||||
@DatabaseExecutor
|
||||
private final Executor dbExecutor;
|
||||
private final MessagingManager messagingManager;
|
||||
private final ImageHelper imageHelper;
|
||||
private final ImageSizeCalculator imageSizeCalculator;
|
||||
@@ -45,17 +50,17 @@ class AttachmentRetrieverImpl implements AttachmentRetriever {
|
||||
private final int minWidth, maxWidth;
|
||||
private final int minHeight, maxHeight;
|
||||
|
||||
// Info for AttachmentItems that are either still LOADING or MISSING
|
||||
private final Map<MessageId, UnavailableItem> unavailableItems =
|
||||
new ConcurrentHashMap<>();
|
||||
// We cache only items in their final state: AVAILABLE or ERROR
|
||||
private final Map<MessageId, AttachmentItem> itemCache =
|
||||
new ConcurrentHashMap<>();
|
||||
private final Map<MessageId, MutableLiveData<AttachmentItem>>
|
||||
itemsWithSize = new ConcurrentHashMap<>();
|
||||
private final Map<MessageId, MutableLiveData<AttachmentItem>>
|
||||
itemsWithoutSize = new ConcurrentHashMap<>();
|
||||
|
||||
@Inject
|
||||
AttachmentRetrieverImpl(MessagingManager messagingManager,
|
||||
AttachmentRetrieverImpl(@DatabaseExecutor Executor dbExecutor,
|
||||
MessagingManager messagingManager,
|
||||
AttachmentDimensions dimensions, ImageHelper imageHelper,
|
||||
ImageSizeCalculator imageSizeCalculator) {
|
||||
this.dbExecutor = dbExecutor;
|
||||
this.messagingManager = messagingManager;
|
||||
this.imageHelper = imageHelper;
|
||||
this.imageSizeCalculator = imageSizeCalculator;
|
||||
@@ -74,20 +79,37 @@ class AttachmentRetrieverImpl implements AttachmentRetriever {
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<AttachmentItem> getAttachmentItems(
|
||||
public List<LiveData<AttachmentItem>> getAttachmentItems(
|
||||
PrivateMessageHeader messageHeader) {
|
||||
List<AttachmentHeader> headers = messageHeader.getAttachmentHeaders();
|
||||
List<AttachmentItem> items = new ArrayList<>(headers.size());
|
||||
List<LiveData<AttachmentItem>> items = new ArrayList<>(headers.size());
|
||||
boolean needsSize = headers.size() == 1;
|
||||
for (AttachmentHeader h : headers) {
|
||||
AttachmentItem item = itemCache.get(h.getMessageId());
|
||||
if (item == null || (needsSize && !item.hasSize())) {
|
||||
item = new AttachmentItem(h, defaultSize, defaultSize, LOADING);
|
||||
UnavailableItem unavailableItem = new UnavailableItem(
|
||||
messageHeader.getId(), h, needsSize);
|
||||
unavailableItems.put(h.getMessageId(), unavailableItem);
|
||||
// try cache for existing item live data
|
||||
MutableLiveData<AttachmentItem> liveData;
|
||||
if (needsSize) liveData = itemsWithSize.get(h.getMessageId());
|
||||
else {
|
||||
// try items with size first, as they work as well
|
||||
liveData = itemsWithSize.get(h.getMessageId());
|
||||
if (liveData == null)
|
||||
liveData = itemsWithoutSize.get(h.getMessageId());
|
||||
}
|
||||
items.add(item);
|
||||
|
||||
// create new live data with LOADING item if cache miss
|
||||
if (liveData == null) {
|
||||
AttachmentItem item = new AttachmentItem(h,
|
||||
defaultSize, defaultSize, LOADING);
|
||||
final MutableLiveData<AttachmentItem> finalLiveData =
|
||||
new MutableLiveData<>(item);
|
||||
// kick-off loading of attachment, will post to live data
|
||||
dbExecutor.execute(
|
||||
() -> loadAttachmentItem(h, needsSize, finalLiveData));
|
||||
// add new LiveData to cache
|
||||
liveData = finalLiveData;
|
||||
if (needsSize) itemsWithSize.put(h.getMessageId(), liveData);
|
||||
else itemsWithoutSize.put(h.getMessageId(), liveData);
|
||||
}
|
||||
items.add(liveData);
|
||||
}
|
||||
return items;
|
||||
}
|
||||
@@ -98,46 +120,63 @@ class AttachmentRetrieverImpl implements AttachmentRetriever {
|
||||
AttachmentHeader h) throws DbException {
|
||||
try {
|
||||
Attachment a = messagingManager.getAttachment(h);
|
||||
// this adds it to the cache automatically
|
||||
createAttachmentItem(a, true);
|
||||
AttachmentItem item = createAttachmentItem(a, true);
|
||||
MutableLiveData<AttachmentItem> liveData =
|
||||
new MutableLiveData<>(item);
|
||||
itemsWithSize.put(h.getMessageId(), liveData);
|
||||
} catch (NoSuchMessageException e) {
|
||||
LOG.info("Attachment not received yet");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
@DatabaseExecutor
|
||||
public Pair<MessageId, AttachmentItem> loadAttachmentItem(
|
||||
MessageId attachmentId) throws DbException {
|
||||
UnavailableItem unavailableItem = unavailableItems.get(attachmentId);
|
||||
if (unavailableItem == null) return null;
|
||||
public void loadAttachmentItem(MessageId attachmentId) {
|
||||
// try to find LiveData for attachment in both caches
|
||||
MutableLiveData<AttachmentItem> liveData;
|
||||
boolean needsSize = true;
|
||||
liveData = itemsWithSize.get(attachmentId);
|
||||
if (liveData == null) {
|
||||
needsSize = false;
|
||||
liveData = itemsWithoutSize.get(attachmentId);
|
||||
}
|
||||
|
||||
MessageId conversationMessageId =
|
||||
unavailableItem.getConversationMessageId();
|
||||
AttachmentHeader h = unavailableItem.getHeader();
|
||||
boolean needsSize = unavailableItem.needsSize();
|
||||
// If no LiveData for the attachment exists,
|
||||
// its message did not yet arrive and we can ignore it for now.
|
||||
if (liveData == null) return;
|
||||
|
||||
// actually load the attachment item
|
||||
AttachmentHeader h = requireNonNull(liveData.getValue()).getHeader();
|
||||
loadAttachmentItem(h, needsSize, liveData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads an {@link AttachmentItem} from the database
|
||||
* and notifies the given {@link LiveData}.
|
||||
*/
|
||||
@DatabaseExecutor
|
||||
private void loadAttachmentItem(AttachmentHeader h, boolean needsSize,
|
||||
MutableLiveData<AttachmentItem> liveData) {
|
||||
Attachment a;
|
||||
AttachmentItem item;
|
||||
try {
|
||||
Attachment a = messagingManager.getAttachment(h);
|
||||
a = messagingManager.getAttachment(h);
|
||||
item = createAttachmentItem(a, needsSize);
|
||||
unavailableItems.remove(attachmentId);
|
||||
} catch (NoSuchMessageException e) {
|
||||
LOG.info("Attachment not received yet");
|
||||
// unavailable item is still tracked, no need to add it again
|
||||
item = new AttachmentItem(h, defaultSize, defaultSize, MISSING);
|
||||
} catch (DbException e) {
|
||||
logException(LOG, WARNING, e);
|
||||
item = new AttachmentItem(h, "", ERROR);
|
||||
}
|
||||
return new Pair<>(conversationMessageId, item);
|
||||
liveData.postValue(item);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AttachmentItem createAttachmentItem(Attachment a,
|
||||
boolean needsSize) {
|
||||
AttachmentItem item;
|
||||
AttachmentHeader h = a.getHeader();
|
||||
AttachmentItem item = itemCache.get(h.getMessageId());
|
||||
if (item != null && (needsSize == item.hasSize())) return item;
|
||||
|
||||
if (needsSize) {
|
||||
InputStream is = new BufferedInputStream(a.getStream());
|
||||
Size size = imageSizeCalculator.getSize(is, h.getContentType());
|
||||
@@ -153,7 +192,6 @@ class AttachmentRetrieverImpl implements AttachmentRetriever {
|
||||
}
|
||||
item = new AttachmentItem(h, extension, state);
|
||||
}
|
||||
itemCache.put(h.getMessageId(), item);
|
||||
return item;
|
||||
}
|
||||
|
||||
|
||||
@@ -96,6 +96,7 @@ import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.core.app.ActivityCompat;
|
||||
import androidx.core.app.ActivityOptionsCompat;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.Observer;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
@@ -130,7 +131,6 @@ import static org.briarproject.bramble.util.StringUtils.isNullOrEmpty;
|
||||
import static org.briarproject.bramble.util.StringUtils.join;
|
||||
import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_ATTACH_IMAGE;
|
||||
import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_INTRODUCTION;
|
||||
import static org.briarproject.briar.android.attachment.AttachmentItem.State.LOADING;
|
||||
import static org.briarproject.briar.android.conversation.ImageActivity.ATTACHMENTS;
|
||||
import static org.briarproject.briar.android.conversation.ImageActivity.ATTACHMENT_POSITION;
|
||||
import static org.briarproject.briar.android.conversation.ImageActivity.DATE;
|
||||
@@ -558,7 +558,8 @@ public class ConversationActivity extends BriarActivity
|
||||
LOG.info("Eagerly loading image size for latest message");
|
||||
AttachmentHeader header = headers.get(0);
|
||||
// get the item to retrieve its size
|
||||
attachmentRetriever.cacheAttachmentItemWithSize(h.getId(), header);
|
||||
attachmentRetriever
|
||||
.cacheAttachmentItemWithSize(h.getId(), header);
|
||||
}
|
||||
} catch (DbException e) {
|
||||
logException(LOG, WARNING, e);
|
||||
@@ -639,33 +640,6 @@ public class ConversationActivity extends BriarActivity
|
||||
&& adapter.isScrolledToBottom(layoutManager);
|
||||
}
|
||||
|
||||
private void loadMessageAttachments(List<AttachmentItem> items) {
|
||||
runOnDbThread(() -> {
|
||||
for (AttachmentItem item : items) {
|
||||
if (item.getState() == LOADING)
|
||||
loadMessageAttachment(item.getMessageId(), false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@DatabaseExecutor
|
||||
private void loadMessageAttachment(MessageId attachmentId,
|
||||
boolean allowedToFail) {
|
||||
try {
|
||||
Pair<MessageId, AttachmentItem> pair = attachmentRetriever
|
||||
.loadAttachmentItem(attachmentId);
|
||||
if (pair == null && allowedToFail) {
|
||||
LOG.warning("Attachment arrived before message");
|
||||
return;
|
||||
} else if (pair == null) throw new AssertionError();
|
||||
MessageId conversationMessageId = pair.getFirst();
|
||||
AttachmentItem item = pair.getSecond();
|
||||
updateMessageAttachment(conversationMessageId, item);
|
||||
} catch (DbException e) {
|
||||
logException(LOG, WARNING, e);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateMessageAttachment(MessageId m, AttachmentItem item) {
|
||||
runOnUiThreadUnlessDestroyed(() -> {
|
||||
Pair<Integer, ConversationMessageItem> pair =
|
||||
@@ -748,9 +722,8 @@ public class ConversationActivity extends BriarActivity
|
||||
|
||||
@UiThread
|
||||
private void onAttachmentReceived(MessageId attachmentId) {
|
||||
// This is allowed to fail, because the conversation message
|
||||
// might arrive *after* the attachment.
|
||||
runOnDbThread(() -> loadMessageAttachment(attachmentId, true));
|
||||
runOnDbThread(
|
||||
() -> attachmentRetriever.loadAttachmentItem(attachmentId));
|
||||
}
|
||||
|
||||
@UiThread
|
||||
@@ -1134,9 +1107,32 @@ public class ConversationActivity extends BriarActivity
|
||||
*/
|
||||
@Override
|
||||
public List<AttachmentItem> getAttachmentItems(PrivateMessageHeader h) {
|
||||
List<AttachmentItem> items = attachmentRetriever.getAttachmentItems(h);
|
||||
loadMessageAttachments(items);
|
||||
List<LiveData<AttachmentItem>> liveDataList =
|
||||
attachmentRetriever.getAttachmentItems(h);
|
||||
List<AttachmentItem> items = new ArrayList<>(liveDataList.size());
|
||||
for (LiveData<AttachmentItem> liveData : liveDataList) {
|
||||
liveData.observe(this, new AttachmentObserver(h.getId(), liveData));
|
||||
items.add(requireNonNull(liveData.getValue()));
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
private class AttachmentObserver implements Observer<AttachmentItem> {
|
||||
private final MessageId conversationMessageId;
|
||||
private final LiveData<AttachmentItem> liveData;
|
||||
|
||||
private AttachmentObserver(MessageId conversationMessageId,
|
||||
LiveData<AttachmentItem> liveData) {
|
||||
this.conversationMessageId = conversationMessageId;
|
||||
this.liveData = liveData;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onChanged(AttachmentItem attachmentItem) {
|
||||
updateMessageAttachment(conversationMessageId, attachmentItem);
|
||||
if (attachmentItem.getState().isFinal())
|
||||
liveData.removeObserver(this);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package org.briarproject.briar.android.attachment;
|
||||
|
||||
import org.briarproject.bramble.api.sync.MessageId;
|
||||
import org.briarproject.bramble.test.BrambleMockTestCase;
|
||||
import org.briarproject.bramble.test.ImmediateExecutor;
|
||||
import org.briarproject.briar.api.messaging.Attachment;
|
||||
import org.briarproject.briar.api.messaging.AttachmentHeader;
|
||||
import org.briarproject.briar.api.messaging.MessagingManager;
|
||||
@@ -11,6 +12,7 @@ import org.junit.Test;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.InputStream;
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
import static org.briarproject.bramble.test.TestUtils.getRandomBytes;
|
||||
import static org.briarproject.bramble.test.TestUtils.getRandomId;
|
||||
@@ -33,8 +35,9 @@ public class AttachmentRetrieverTest extends BrambleMockTestCase {
|
||||
MessagingManager messagingManager =
|
||||
context.mock(MessagingManager.class);
|
||||
imageSizeCalculator = context.mock(ImageSizeCalculator.class);
|
||||
retriever = new AttachmentRetrieverImpl(messagingManager, dimensions,
|
||||
imageHelper, imageSizeCalculator);
|
||||
Executor dbExecutor = new ImmediateExecutor();
|
||||
retriever = new AttachmentRetrieverImpl(dbExecutor, messagingManager,
|
||||
dimensions, imageHelper, imageSizeCalculator);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
Reference in New Issue
Block a user