Commit 55f4600a authored by Torsten Grote's avatar Torsten Grote

[android] Create attachments before showing previews

parent 249e1e28
package org.briarproject.briar.android.blog;
import android.net.Uri;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
......@@ -21,6 +20,7 @@ import org.briarproject.briar.android.fragment.BaseFragment;
import org.briarproject.briar.android.view.TextInputView;
import org.briarproject.briar.android.view.TextSendController;
import org.briarproject.briar.android.view.TextSendController.SendListener;
import org.briarproject.briar.api.messaging.AttachmentHeader;
import java.util.List;
......@@ -121,7 +121,8 @@ public class ReblogFragment extends BaseFragment implements SendListener {
}
@Override
public void onSendClick(@Nullable String text, List<Uri> imageUris) {
public void onSendClick(@Nullable String text,
List<AttachmentHeader> headers) {
ui.input.hideSoftKeyboard();
feedController.repeatPost(item, text,
new UiExceptionHandler<DbException>(this) {
......
package org.briarproject.briar.android.blog;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.view.KeyEvent;
......@@ -27,6 +26,7 @@ import org.briarproject.briar.api.android.AndroidNotificationManager;
import org.briarproject.briar.api.blog.BlogManager;
import org.briarproject.briar.api.blog.BlogPost;
import org.briarproject.briar.api.blog.BlogPostFactory;
import org.briarproject.briar.api.messaging.AttachmentHeader;
import java.security.GeneralSecurityException;
import java.util.List;
......@@ -120,7 +120,8 @@ public class WriteBlogPostActivity extends BriarActivity
}
@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();
// hide publish button, show progress bar
......
package org.briarproject.briar.android.conversation;
import android.content.ContentResolver;
import android.graphics.BitmapFactory;
import android.graphics.BitmapFactory.Options;
import android.net.Uri;
import android.support.annotation.Nullable;
import android.support.annotation.VisibleForTesting;
import android.support.media.ExifInterface;
import android.webkit.MimeTypeMap;
......@@ -12,6 +15,7 @@ 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;
import org.briarproject.bramble.api.sync.GroupId;
import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.briar.android.conversation.ImageHelper.DecodeResult;
import org.briarproject.briar.api.messaging.Attachment;
......@@ -25,6 +29,7 @@ import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.logging.Logger;
import static android.support.media.ExifInterface.ORIENTATION_ROTATE_270;
......@@ -54,6 +59,7 @@ class AttachmentController {
private final int minWidth, maxWidth;
private final int minHeight, maxHeight;
private final List<AttachmentHeader> unsent = new CopyOnWriteArrayList<>();
private final Map<MessageId, List<AttachmentItem>> attachmentCache =
new ConcurrentHashMap<>();
......@@ -114,6 +120,54 @@ class AttachmentController {
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.
* <p>
......@@ -135,6 +189,7 @@ class AttachmentController {
* Creates an {@link AttachmentItem} from the {@link Attachment}'s
* {@link InputStream} which will be closed when this method returns.
*/
@VisibleForTesting
AttachmentItem getAttachmentItem(AttachmentHeader h, Attachment a,
boolean needsSize) {
MessageId messageId = h.getMessageId();
......
......@@ -6,7 +6,6 @@ import android.arch.lifecycle.ViewModelProvider;
import android.arch.lifecycle.ViewModelProviders;
import android.content.DialogInterface;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.os.Parcelable;
import android.support.annotation.Nullable;
......@@ -264,7 +263,7 @@ public class ConversationActivity extends BriarActivity
if (FEATURE_FLAG_IMAGE_ATTACHMENTS) {
ImagePreview imagePreview = findViewById(R.id.imagePreview);
sendController = new TextAttachmentController(textInputView,
imagePreview, this, this);
imagePreview, this, this, viewModel);
observeOnce(viewModel.hasImageSupport(), this, hasSupport -> {
if (hasSupport != null && hasSupport) {
// remove cast when removing FEATURE_FLAG_IMAGE_ATTACHMENTS
......@@ -658,12 +657,13 @@ public class ConversationActivity extends BriarActivity
}
@Override
public void onSendClick(@Nullable String text, List<Uri> imageUris) {
if (isNullOrEmpty(text) && imageUris.isEmpty())
public void onSendClick(@Nullable String text,
List<AttachmentHeader> attachmentHeaders) {
if (isNullOrEmpty(text) && attachmentHeaders.isEmpty())
throw new AssertionError();
long timestamp = System.currentTimeMillis();
timestamp = Math.max(timestamp, getMinTimestampForNewMessage());
viewModel.sendMessage(text, imageUris, timestamp);
viewModel.sendMessage(text, attachmentHeaders, timestamp);
textInputView.clearText();
}
......
......@@ -11,7 +11,6 @@ import android.support.annotation.Nullable;
import android.support.annotation.UiThread;
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.ContactId;
import org.briarproject.bramble.api.contact.ContactManager;
......@@ -27,10 +26,11 @@ import org.briarproject.bramble.api.settings.SettingsManager;
import org.briarproject.bramble.api.sync.GroupId;
import org.briarproject.bramble.api.sync.Message;
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.view.TextAttachmentController.AttachmentManager;
import org.briarproject.briar.android.viewmodel.LiveEvent;
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.MessagingManager;
import org.briarproject.briar.api.messaging.PrivateMessage;
......@@ -38,10 +38,11 @@ import org.briarproject.briar.api.messaging.PrivateMessageFactory;
import org.briarproject.briar.api.messaging.PrivateMessageHeader;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Executor;
import java.util.logging.Logger;
......@@ -50,7 +51,6 @@ import javax.inject.Inject;
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.logDuration;
import static org.briarproject.bramble.util.LogUtils.logException;
import static org.briarproject.bramble.util.LogUtils.now;
......@@ -59,7 +59,8 @@ import static org.briarproject.briar.android.settings.SettingsFragment.SETTINGS_
import static org.briarproject.briar.android.util.UiUtils.observeForeverOnce;
@NotNullByDefault
public class ConversationViewModel extends AndroidViewModel {
public class ConversationViewModel extends AndroidViewModel implements
AttachmentManager {
private static Logger LOG =
getLogger(ConversationViewModel.class.getName());
......@@ -73,6 +74,7 @@ public class ConversationViewModel extends AndroidViewModel {
@CryptoExecutor
private final Executor cryptoExecutor;
private final TransactionManager db;
private final AndroidExecutor androidExecutor;
private final MessagingManager messagingManager;
private final ContactManager contactManager;
private final SettingsManager settingsManager;
......@@ -100,17 +102,20 @@ public class ConversationViewModel extends AndroidViewModel {
new MutableLiveData<>();
private final MutableLiveData<PrivateMessageHeader> addedHeader =
new MutableLiveData<>();
// TODO move to AttachmentController
private final Map<Uri, AttachmentItem> unsentItems = new HashMap<>();
@Inject
ConversationViewModel(Application application,
@DatabaseExecutor Executor dbExecutor,
@CryptoExecutor Executor cryptoExecutor, TransactionManager db,
MessagingManager messagingManager, ContactManager contactManager,
SettingsManager settingsManager,
AndroidExecutor androidExecutor, MessagingManager messagingManager,
ContactManager contactManager, SettingsManager settingsManager,
PrivateMessageFactory privateMessageFactory) {
super(application);
this.dbExecutor = dbExecutor;
this.cryptoExecutor = cryptoExecutor;
this.androidExecutor = androidExecutor;
this.db = db;
this.messagingManager = messagingManager;
this.contactManager = contactManager;
......@@ -121,6 +126,12 @@ public class ConversationViewModel extends AndroidViewModel {
contactDeleted.setValue(false);
}
@Override
protected void onCleared() {
super.onCleared();
attachmentController.deleteUnsentAttachments();
}
/**
* Setting the {@link ContactId} automatically triggers loading of other
* data.
......@@ -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();
observeForeverOnce(messagingGroupId, groupId -> {
if (groupId == null) return;
// calls through to creating and storing the message
storeAttachments(groupId, text, uris, timestamp);
createMessage(groupId, text, attachmentHeaders, 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() {
if (contactId == null) throw new IllegalStateException();
dbExecutor.execute(() -> {
......@@ -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,
List<AttachmentHeader> attachments, List<AttachmentItem> aItems,
long timestamp) {
List<AttachmentHeader> attachments, long timestamp) {
cryptoExecutor.execute(() -> {
try {
// TODO remove when text can be null in the backend
......@@ -311,7 +313,6 @@ public class ConversationViewModel extends AndroidViewModel {
PrivateMessage pm = privateMessageFactory
.createPrivateMessage(groupId, timestamp, msgText,
attachments);
attachmentController.put(pm.getMessage().getId(), aItems);
storeMessage(pm, msgText, attachments);
} catch (FormatException e) {
throw new RuntimeException(e);
......@@ -331,6 +332,10 @@ public class ConversationViewModel extends AndroidViewModel {
message.getId(), message.getGroupId(),
message.getTimestamp(), true, true, false, false,
text != null, attachments);
attachmentController.put(m.getMessage().getId(),
new ArrayList<>(unsentItems.values()));
unsentItems.clear();
attachmentController.markAttachmentsSent();
// TODO add text to cache when available here
addedHeader.postValue(h);
} catch (DbException e) {
......
package org.briarproject.briar.android.introduction;
import android.content.Context;
import android.net.Uri;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v7.app.ActionBar;
......@@ -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.SendListener;
import org.briarproject.briar.api.introduction.IntroductionManager;
import org.briarproject.briar.api.messaging.AttachmentHeader;
import java.util.List;
import java.util.logging.Logger;
......@@ -193,7 +193,8 @@ public class IntroductionMessageFragment extends BaseFragment
}
@Override
public void onSendClick(@Nullable String text, List<Uri> imageUris) {
public void onSendClick(@Nullable String text,
List<AttachmentHeader> headers) {
// disable button to prevent accidental double invitations
ui.message.setReady(false);
......
package org.briarproject.briar.android.sharing;
import android.content.Context;
import android.net.Uri;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.annotation.StringRes;
......@@ -19,6 +18,7 @@ import org.briarproject.briar.android.fragment.BaseFragment;
import org.briarproject.briar.android.view.LargeTextInputView;
import org.briarproject.briar.android.view.TextSendController;
import org.briarproject.briar.android.view.TextSendController.SendListener;
import org.briarproject.briar.api.messaging.AttachmentHeader;
import java.util.List;
......@@ -83,7 +83,8 @@ public abstract class BaseMessageFragment extends BaseFragment
}
@Override
public void onSendClick(@Nullable String text, List<Uri> imageUris) {
public void onSendClick(@Nullable String text,
List<AttachmentHeader> headers) {
// disable button to prevent accidental double actions
sendController.setReady(false);
message.hideSoftKeyboard();
......
package org.briarproject.briar.android.threaded;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.os.Parcelable;
import android.support.annotation.CallSuper;
......@@ -34,6 +33,7 @@ import org.briarproject.briar.android.view.TextSendController;
import org.briarproject.briar.android.view.TextSendController.SendListener;
import org.briarproject.briar.android.view.UnreadMessageButton;
import org.briarproject.briar.api.client.NamedGroup;
import org.briarproject.briar.api.messaging.AttachmentHeader;
import java.util.Collection;
import java.util.List;
......@@ -341,7 +341,8 @@ public abstract class ThreadListActivity<G extends NamedGroup, I extends ThreadI
}
@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();
I replyItem = adapter.getHighlightedItem();
......
package org.briarproject.briar.android.view;
import android.content.Context;
import android.net.Uri;
import android.support.annotation.Nullable;
import android.support.constraint.ConstraintLayout;
import android.support.v7.widget.RecyclerView;
......@@ -60,21 +59,22 @@ public class ImagePreview extends ConstraintLayout {
this.listener = listener;
}
void showPreview(Collection<Uri> imageUris) {
void showPreview(Collection<ImagePreviewItem> items) {
if (listener == null) throw new IllegalStateException();
if (imageUris.size() == 1) {
if (items.size() == 1) {
LayoutParams params = (LayoutParams) imageList.getLayoutParams();
params.width = MATCH_PARENT;
imageList.setLayoutParams(params);
}
setVisibility(VISIBLE);
imageList.setAdapter(new ImagePreviewAdapter(imageUris, listener));
ImagePreviewAdapter adapter = new ImagePreviewAdapter(items, listener);
imageList.setAdapter(adapter);
}
void removeUri(Uri uri) {
void loadPreviewImage(ImagePreviewItem item) {
ImagePreviewAdapter adapter =
(ImagePreviewAdapter) imageList.getAdapter();
requireNonNull(adapter).removeUri(uri);
((ImagePreviewAdapter) imageList.getAdapter());
requireNonNull(adapter).loadItemPreview(item);
}
interface ImagePreviewListener {
......@@ -86,7 +86,7 @@ public class ImagePreview extends ConstraintLayout {
*
* Warning: Glide may call this multiple times.
*/
void onUriError(Uri uri);
void onError();
void onCancel();
}
......
package org.briarproject.briar.android.view;
import android.net.Uri;
import android.support.annotation.LayoutRes;
import android.support.v7.widget.RecyclerView.Adapter;
import android.view.LayoutInflater;
......@@ -15,17 +14,19 @@ import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import static android.support.v7.widget.RecyclerView.NO_POSITION;
import static java.util.Objects.requireNonNull;
@NotNullByDefault
class ImagePreviewAdapter extends Adapter<ImagePreviewViewHolder> {
private final List<Uri> items;
private final List<ImagePreviewItem> items;
private final ImagePreviewListener listener;
@LayoutRes
private final int layout;
ImagePreviewAdapter(Collection<Uri> items, ImagePreviewListener listener) {
ImagePreviewAdapter(Collection<ImagePreviewItem> items,
ImagePreviewListener listener) {
this.items = new ArrayList<>(items);
this.listener = listener;
this.layout = items.size() == 1 ?
......@@ -52,11 +53,12 @@ class ImagePreviewAdapter extends Adapter<ImagePreviewViewHolder> {
return items.size();
}
void removeUri(Uri uri) {
int pos = items.indexOf(uri);
if (pos == -1) return;
items.remove(uri);
notifyItemRemoved(pos);
void loadItemPreview(ImagePreviewItem item) {
int pos = items.indexOf(item);
if (pos == NO_POSITION) throw new AssertionError();
ImagePreviewItem newItem = items.get(pos);
newItem.setWaitForLoading(false);
notifyItemChanged(pos, newItem);
}
}
package org.briarproject.briar.android.view;
import android.net.Uri;
import android.support.annotation.Nullable;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
@NotNullByDefault
class ImagePreviewItem {
private final Uri uri;
private boolean waitForLoading = true;
private ImagePreviewItem(Uri uri) {
this.uri = uri;
}
Uri getUri() {
return uri;
}
void setWaitForLoading(boolean waitForLoading) {
this.waitForLoading = waitForLoading;
}
boolean waitForLoading() {
return waitForLoading;
}
static List<ImagePreviewItem> fromUris(Collection<Uri> uris) {
List<ImagePreviewItem> items = new ArrayList<>(uris.size());
for (Uri uri : uris) {
items.add(new ImagePreviewItem(uri));
}
return items;
}
@Override
public boolean equals(@Nullable Object o) {
return o instanceof ImagePreviewItem &&
uri.equals(((ImagePreviewItem) o).uri);
}
@Override
public int hashCode() {
return uri.hashCode();
}
}
package org.briarproject.briar.android.view;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.support.annotation.DrawableRes;
import android.support.annotation.Nullable;
import android.support.v7.widget.RecyclerView.ViewHolder;
......@@ -42,9 +41,10 @@ class ImagePreviewViewHolder extends ViewHolder {
this.progressBar = v.findViewById(R.id.progressBar);
}
void bind(Uri uri) {
void bind(ImagePreviewItem item) {
if (item.waitForLoading()) return;
GlideApp.with(imageView)
.load(uri)
.load(item.getUri())
.diskCacheStrategy(NONE)
.error(ERROR_RES)
.downsample(FIT_CENTER)
......@@ -55,7 +55,7 @@ class ImagePreviewViewHolder extends ViewHolder {
Object model, Target<Drawable> target,
boolean isFirstResource) {
progressBar.setVisibility(INVISIBLE);
listener.onUriError(uri);
listener.onError();
return false;
}
......
......@@ -16,6 +16,7 @@ import android.widget.Toast;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.briar.R;
import org.briarproject.briar.android.view.ImagePreview.ImagePreviewListener;
import org.briarproject.briar.api.messaging.AttachmentHeader;