Jump to the oldest unread message after loading a draft.

This commit is contained in:
Greyson Parrelli 2018-07-25 11:30:48 -04:00
parent d5a9efa96a
commit 67190774cc
6 changed files with 185 additions and 42 deletions

View File

@ -180,6 +180,9 @@ import java.util.Collections;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import static org.thoughtcrime.securesms.TransportOption.Type; import static org.thoughtcrime.securesms.TransportOption.Type;
import static org.thoughtcrime.securesms.database.GroupDatabase.GroupRecord; import static org.thoughtcrime.securesms.database.GroupDatabase.GroupRecord;
@ -294,7 +297,18 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
@Override @Override
public void onSuccess(Boolean result) { public void onSuccess(Boolean result) {
initializeProfiles(); initializeProfiles();
initializeDraft(); initializeDraft().addListener(new AssertedSuccessListener<Boolean>() {
@Override
public void onSuccess(Boolean result) {
if (result != null && result) {
Util.runOnMain(() -> {
if (fragment != null && fragment.isResumed()) {
fragment.moveToLastSeen();
}
});
}
}
});
} }
}); });
} }
@ -983,19 +997,29 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
///// Initializers ///// Initializers
private void initializeDraft() { private ListenableFuture<Boolean> initializeDraft() {
final SettableFuture<Boolean> result = new SettableFuture<>();
final String draftText = getIntent().getStringExtra(TEXT_EXTRA); final String draftText = getIntent().getStringExtra(TEXT_EXTRA);
final Uri draftMedia = getIntent().getData(); final Uri draftMedia = getIntent().getData();
final MediaType draftMediaType = MediaType.from(getIntent().getType()); final MediaType draftMediaType = MediaType.from(getIntent().getType());
if (draftText != null) composeText.setText(draftText); if (draftText != null) {
if (draftMedia != null && draftMediaType != null) setMedia(draftMedia, draftMediaType); composeText.setText(draftText);
result.set(true);
}
if (draftMedia != null && draftMediaType != null) {
return setMedia(draftMedia, draftMediaType);
}
if (draftText == null && draftMedia == null && draftMediaType == null) { if (draftText == null && draftMedia == null && draftMediaType == null) {
initializeDraftFromDatabase(); return initializeDraftFromDatabase();
} else { } else {
updateToggleButtonState(); updateToggleButtonState();
result.set(false);
} }
return result;
} }
private void initializeEnabledCheck() { private void initializeEnabledCheck() {
@ -1005,7 +1029,9 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
attachButton.setEnabled(enabled); attachButton.setEnabled(enabled);
} }
private void initializeDraftFromDatabase() { private ListenableFuture<Boolean> initializeDraftFromDatabase() {
SettableFuture<Boolean> future = new SettableFuture<>();
new AsyncTask<Void, Void, List<Draft>>() { new AsyncTask<Void, Void, List<Draft>>() {
@Override @Override
protected List<Draft> doInBackground(Void... params) { protected List<Draft> doInBackground(Void... params) {
@ -1019,26 +1045,42 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
@Override @Override
protected void onPostExecute(List<Draft> drafts) { protected void onPostExecute(List<Draft> drafts) {
AtomicInteger draftsRemaining = new AtomicInteger(drafts.size());
AtomicBoolean success = new AtomicBoolean(false);
ListenableFuture.Listener<Boolean> listener = new AssertedSuccessListener<Boolean>() {
@Override
public void onSuccess(Boolean result) {
success.compareAndSet(false, result);
if (draftsRemaining.decrementAndGet() <= 0) {
future.set(success.get());
}
}
};
for (Draft draft : drafts) { for (Draft draft : drafts) {
try { try {
switch (draft.getType()) { switch (draft.getType()) {
case Draft.TEXT: case Draft.TEXT:
composeText.setText(draft.getValue()); composeText.setText(draft.getValue());
listener.onSuccess(true);
break; break;
case Draft.LOCATION: case Draft.LOCATION:
attachmentManager.setLocation(SignalPlace.deserialize(draft.getValue()), getCurrentMediaConstraints()); attachmentManager.setLocation(SignalPlace.deserialize(draft.getValue()), getCurrentMediaConstraints()).addListener(listener);
break; break;
case Draft.IMAGE: case Draft.IMAGE:
setMedia(Uri.parse(draft.getValue()), MediaType.IMAGE); setMedia(Uri.parse(draft.getValue()), MediaType.IMAGE).addListener(listener);
break; break;
case Draft.AUDIO: case Draft.AUDIO:
setMedia(Uri.parse(draft.getValue()), MediaType.AUDIO); setMedia(Uri.parse(draft.getValue()), MediaType.AUDIO).addListener(listener);
break; break;
case Draft.VIDEO: case Draft.VIDEO:
setMedia(Uri.parse(draft.getValue()), MediaType.VIDEO); setMedia(Uri.parse(draft.getValue()), MediaType.VIDEO).addListener(listener);
break; break;
case Draft.QUOTE: case Draft.QUOTE:
new QuoteRestorationTask(draft.getValue()).execute(); SettableFuture<Boolean> quoteResult = new SettableFuture<>();
new QuoteRestorationTask(draft.getValue(), quoteResult).execute();
quoteResult.addListener(listener);
break; break;
} }
} catch (IOException e) { } catch (IOException e) {
@ -1049,6 +1091,8 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
updateToggleButtonState(); updateToggleButtonState();
} }
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
return future;
} }
private ListenableFuture<Boolean> initializeSecurity(final boolean currentSecureText, private ListenableFuture<Boolean> initializeSecurity(final boolean currentSecureText,
@ -1384,17 +1428,20 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
} }
} }
private void setMedia(@Nullable Uri uri, @NonNull MediaType mediaType) { private ListenableFuture<Boolean> setMedia(@Nullable Uri uri, @NonNull MediaType mediaType) {
setMedia(uri, mediaType, 0, 0); return setMedia(uri, mediaType, 0, 0);
} }
private void setMedia(@Nullable Uri uri, @NonNull MediaType mediaType, int width, int height) { private ListenableFuture<Boolean> setMedia(@Nullable Uri uri, @NonNull MediaType mediaType, int width, int height) {
if (uri == null) return; if (uri == null) {
return new SettableFuture<>(false);
}
if (MediaType.VCARD.equals(mediaType) && isSecureText) { if (MediaType.VCARD.equals(mediaType) && isSecureText) {
openContactShareEditor(uri); openContactShareEditor(uri);
return new SettableFuture<>(false);
} else { } else {
attachmentManager.setMedia(glideRequests, uri, mediaType, getCurrentMediaConstraints(), width, height); return attachmentManager.setMedia(glideRequests, uri, mediaType, getCurrentMediaConstraints(), width, height);
} }
} }
@ -2183,10 +2230,12 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
private class QuoteRestorationTask extends AsyncTask<Void, Void, MessageRecord> { private class QuoteRestorationTask extends AsyncTask<Void, Void, MessageRecord> {
private final String serialized; private final String serialized;
private final SettableFuture<Boolean> future;
QuoteRestorationTask(@NonNull String serialized) { QuoteRestorationTask(@NonNull String serialized, @NonNull SettableFuture<Boolean> future) {
this.serialized = serialized; this.serialized = serialized;
this.future = future;
} }
@Override @Override
@ -2204,8 +2253,10 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
protected void onPostExecute(MessageRecord messageRecord) { protected void onPostExecute(MessageRecord messageRecord) {
if (messageRecord != null) { if (messageRecord != null) {
handleReplyMessage(messageRecord); handleReplyMessage(messageRecord);
future.set(true);
} else { } else {
Log.e(TAG, "Failed to restore a quote from a draft. No matching message record."); Log.e(TAG, "Failed to restore a quote from a draft. No matching message record.");
future.set(false);
} }
} }
} }

View File

@ -190,6 +190,13 @@ public class ConversationFragment extends Fragment
getLoaderManager().restartLoader(0, Bundle.EMPTY, this); getLoaderManager().restartLoader(0, Bundle.EMPTY, this);
} }
public void moveToLastSeen() {
if (lastSeen > 0) {
int position = getListAdapter().findLastSeenPosition(lastSeen);
scrollToLastSeenPosition(position);
}
}
private void initializeResources() { private void initializeResources() {
this.recipient = Recipient.from(getActivity(), getActivity().getIntent().getParcelableExtra(ConversationActivity.ADDRESS_EXTRA), true); this.recipient = Recipient.from(getActivity(), getActivity().getIntent().getParcelableExtra(ConversationActivity.ADDRESS_EXTRA), true);
this.threadId = this.getActivity().getIntent().getLongExtra(ConversationActivity.THREAD_ID_EXTRA, -1); this.threadId = this.getActivity().getIntent().getLongExtra(ConversationActivity.THREAD_ID_EXTRA, -1);

View File

@ -0,0 +1,32 @@
package org.thoughtcrime.securesms.components;
import android.graphics.drawable.Drawable;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.widget.ImageView;
import com.bumptech.glide.request.target.DrawableImageViewTarget;
import org.thoughtcrime.securesms.util.concurrent.SettableFuture;
public class GlideListeningTarget extends DrawableImageViewTarget {
private final SettableFuture<Boolean> loaded;
public GlideListeningTarget(@NonNull ImageView view, @NonNull SettableFuture<Boolean> loaded) {
super(view);
this.loaded = loaded;
}
@Override
protected void setResource(@Nullable Drawable resource) {
super.setResource(resource);
loaded.set(true);
}
@Override
public void onLoadFailed(@Nullable Drawable errorDrawable) {
super.onLoadFailed(errorDrawable);
loaded.set(true);
}
}

View File

@ -5,6 +5,7 @@ import android.content.res.TypedArray;
import android.net.Uri; import android.net.Uri;
import android.support.annotation.DrawableRes; import android.support.annotation.DrawableRes;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.UiThread; import android.support.annotation.UiThread;
import android.util.AttributeSet; import android.util.AttributeSet;
import android.util.Log; import android.util.Log;
@ -14,12 +15,16 @@ import android.widget.FrameLayout;
import android.widget.ImageView; import android.widget.ImageView;
import com.bumptech.glide.RequestBuilder; import com.bumptech.glide.RequestBuilder;
import com.bumptech.glide.load.DataSource;
import com.bumptech.glide.load.engine.DiskCacheStrategy; import com.bumptech.glide.load.engine.DiskCacheStrategy;
import com.bumptech.glide.load.engine.GlideException;
import com.bumptech.glide.load.resource.bitmap.BitmapTransformation; import com.bumptech.glide.load.resource.bitmap.BitmapTransformation;
import com.bumptech.glide.load.resource.bitmap.CenterCrop; import com.bumptech.glide.load.resource.bitmap.CenterCrop;
import com.bumptech.glide.load.resource.bitmap.FitCenter; import com.bumptech.glide.load.resource.bitmap.FitCenter;
import com.bumptech.glide.load.resource.bitmap.RoundedCorners; import com.bumptech.glide.load.resource.bitmap.RoundedCorners;
import com.bumptech.glide.request.RequestListener;
import com.bumptech.glide.request.RequestOptions; import com.bumptech.glide.request.RequestOptions;
import com.bumptech.glide.request.target.Target;
import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.database.AttachmentDatabase; import org.thoughtcrime.securesms.database.AttachmentDatabase;
@ -30,6 +35,8 @@ import org.thoughtcrime.securesms.mms.Slide;
import org.thoughtcrime.securesms.mms.SlideClickListener; import org.thoughtcrime.securesms.mms.SlideClickListener;
import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.ViewUtil; import org.thoughtcrime.securesms.util.ViewUtil;
import org.thoughtcrime.securesms.util.concurrent.ListenableFuture;
import org.thoughtcrime.securesms.util.concurrent.SettableFuture;
import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.libsignal.util.guava.Optional;
import java.util.Locale; import java.util.Locale;
@ -221,16 +228,16 @@ public class ThumbnailView extends FrameLayout {
} }
@UiThread @UiThread
public void setImageResource(@NonNull GlideRequests glideRequests, @NonNull Slide slide, public ListenableFuture<Boolean> setImageResource(@NonNull GlideRequests glideRequests, @NonNull Slide slide,
boolean showControls, boolean isPreview) boolean showControls, boolean isPreview)
{ {
setImageResource(glideRequests, slide, showControls, isPreview, 0, 0); return setImageResource(glideRequests, slide, showControls, isPreview, 0, 0);
} }
@UiThread @UiThread
public void setImageResource(@NonNull GlideRequests glideRequests, @NonNull Slide slide, public ListenableFuture<Boolean> setImageResource(@NonNull GlideRequests glideRequests, @NonNull Slide slide,
boolean showControls, boolean isPreview, int naturalWidth, boolean showControls, boolean isPreview, int naturalWidth,
int naturalHeight) int naturalHeight)
{ {
if (showControls) { if (showControls) {
getTransferControls().setSlide(slide); getTransferControls().setSlide(slide);
@ -249,7 +256,7 @@ public class ThumbnailView extends FrameLayout {
if (Util.equals(slide, this.slide)) { if (Util.equals(slide, this.slide)) {
Log.w(TAG, "Not re-loading slide " + slide.asAttachment().getDataUri()); Log.w(TAG, "Not re-loading slide " + slide.asAttachment().getDataUri());
return; return new SettableFuture<>(false);
} }
if (this.slide != null && this.slide.getFastPreflightId() != null && if (this.slide != null && this.slide.getFastPreflightId() != null &&
@ -257,7 +264,7 @@ public class ThumbnailView extends FrameLayout {
{ {
Log.w(TAG, "Not re-loading slide for fast preflight: " + slide.getFastPreflightId()); Log.w(TAG, "Not re-loading slide for fast preflight: " + slide.getFastPreflightId());
this.slide = slide; this.slide = slide;
return; return new SettableFuture<>(false);
} }
Log.w(TAG, "loading part with id " + slide.asAttachment().getDataUri() Log.w(TAG, "loading part with id " + slide.asAttachment().getDataUri()
@ -270,19 +277,31 @@ public class ThumbnailView extends FrameLayout {
dimens[HEIGHT] = naturalHeight; dimens[HEIGHT] = naturalHeight;
invalidate(); invalidate();
if (slide.getThumbnailUri() != null) buildThumbnailGlideRequest(glideRequests, slide).into(image); SettableFuture<Boolean> result = new SettableFuture<>();
else if (slide.hasPlaceholder()) buildPlaceholderGlideRequest(glideRequests, slide).into(image);
else glideRequests.clear(image);
if (slide.getThumbnailUri() != null) {
buildThumbnailGlideRequest(glideRequests, slide).into(new GlideListeningTarget(image, result));
} else if (slide.hasPlaceholder()) {
buildPlaceholderGlideRequest(glideRequests, slide).into(new GlideListeningTarget(image, result));
} else {
glideRequests.clear(image);
result.set(false);
}
return result;
} }
public void setImageResource(@NonNull GlideRequests glideRequests, @NonNull Uri uri) { public ListenableFuture<Boolean> setImageResource(@NonNull GlideRequests glideRequests, @NonNull Uri uri) {
SettableFuture<Boolean> future = new SettableFuture<>();
if (transferControls.isPresent()) getTransferControls().setVisibility(View.GONE); if (transferControls.isPresent()) getTransferControls().setVisibility(View.GONE);
glideRequests.load(new DecryptableUri(uri)) glideRequests.load(new DecryptableUri(uri))
.diskCacheStrategy(DiskCacheStrategy.NONE) .diskCacheStrategy(DiskCacheStrategy.NONE)
.transforms(new CenterCrop(), new RoundedCorners(radius)) .transforms(new CenterCrop(), new RoundedCorners(radius))
.transition(withCrossFade()) .transition(withCrossFade())
.into(image); .into(new GlideListeningTarget(image, future));
return future;
} }
public void setThumbnailClickListener(SlideClickListener listener) { public void setThumbnailClickListener(SlideClickListener listener) {

View File

@ -63,6 +63,7 @@ import org.thoughtcrime.securesms.util.ViewUtil;
import org.thoughtcrime.securesms.util.concurrent.AssertedSuccessListener; import org.thoughtcrime.securesms.util.concurrent.AssertedSuccessListener;
import org.thoughtcrime.securesms.util.concurrent.ListenableFuture; import org.thoughtcrime.securesms.util.concurrent.ListenableFuture;
import org.thoughtcrime.securesms.util.concurrent.ListenableFuture.Listener; import org.thoughtcrime.securesms.util.concurrent.ListenableFuture.Listener;
import org.thoughtcrime.securesms.util.concurrent.SettableFuture;
import org.thoughtcrime.securesms.util.views.Stub; import org.thoughtcrime.securesms.util.views.Stub;
import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.libsignal.util.guava.Optional;
@ -181,12 +182,13 @@ public class AttachmentManager {
this.slide = Optional.of(slide); this.slide = Optional.of(slide);
} }
public void setLocation(@NonNull final SignalPlace place, public ListenableFuture<Boolean> setLocation(@NonNull final SignalPlace place,
@NonNull final MediaConstraints constraints) @NonNull final MediaConstraints constraints)
{ {
inflateStub(); inflateStub();
ListenableFuture<Bitmap> future = mapView.display(place); SettableFuture<Boolean> returnResult = new SettableFuture<>();
ListenableFuture<Bitmap> future = mapView.display(place);
attachmentViewStub.get().setVisibility(View.VISIBLE); attachmentViewStub.get().setVisibility(View.VISIBLE);
removableMediaView.display(mapView, false); removableMediaView.display(mapView, false);
@ -201,20 +203,25 @@ public class AttachmentManager {
setSlide(locationSlide); setSlide(locationSlide);
attachmentListener.onAttachmentChanged(); attachmentListener.onAttachmentChanged();
returnResult.set(true);
} }
}); });
return returnResult;
} }
@SuppressLint("StaticFieldLeak") @SuppressLint("StaticFieldLeak")
public void setMedia(@NonNull final GlideRequests glideRequests, public ListenableFuture<Boolean> setMedia(@NonNull final GlideRequests glideRequests,
@NonNull final Uri uri, @NonNull final Uri uri,
@NonNull final MediaType mediaType, @NonNull final MediaType mediaType,
@NonNull final MediaConstraints constraints, @NonNull final MediaConstraints constraints,
final int width, final int width,
final int height) final int height)
{ {
inflateStub(); inflateStub();
final SettableFuture<Boolean> result = new SettableFuture<>();
new AsyncTask<Void, Void, Slide>() { new AsyncTask<Void, Void, Slide>() {
@Override @Override
protected void onPreExecute() { protected void onPreExecute() {
@ -247,11 +254,13 @@ public class AttachmentManager {
Toast.makeText(context, Toast.makeText(context,
R.string.ConversationActivity_sorry_there_was_an_error_setting_your_attachment, R.string.ConversationActivity_sorry_there_was_an_error_setting_your_attachment,
Toast.LENGTH_SHORT).show(); Toast.LENGTH_SHORT).show();
result.set(false);
} else if (!areConstraintsSatisfied(context, slide, constraints)) { } else if (!areConstraintsSatisfied(context, slide, constraints)) {
attachmentViewStub.get().setVisibility(View.GONE); attachmentViewStub.get().setVisibility(View.GONE);
Toast.makeText(context, Toast.makeText(context,
R.string.ConversationActivity_attachment_exceeds_size_limits, R.string.ConversationActivity_attachment_exceeds_size_limits,
Toast.LENGTH_SHORT).show(); Toast.LENGTH_SHORT).show();
result.set(false);
} else { } else {
setSlide(slide); setSlide(slide);
attachmentViewStub.get().setVisibility(View.VISIBLE); attachmentViewStub.get().setVisibility(View.VISIBLE);
@ -259,12 +268,14 @@ public class AttachmentManager {
if (slide.hasAudio()) { if (slide.hasAudio()) {
audioView.setAudio((AudioSlide) slide, false); audioView.setAudio((AudioSlide) slide, false);
removableMediaView.display(audioView, false); removableMediaView.display(audioView, false);
result.set(true);
} else if (slide.hasDocument()) { } else if (slide.hasDocument()) {
documentView.setDocument((DocumentSlide) slide, false); documentView.setDocument((DocumentSlide) slide, false);
removableMediaView.display(documentView, false); removableMediaView.display(documentView, false);
result.set(true);
} else { } else {
Attachment attachment = slide.asAttachment(); Attachment attachment = slide.asAttachment();
thumbnail.setImageResource(glideRequests, slide, false, true, attachment.getWidth(), attachment.getHeight()); result.deferTo(thumbnail.setImageResource(glideRequests, slide, false, true, attachment.getWidth(), attachment.getHeight()));
removableMediaView.display(thumbnail, mediaType == MediaType.IMAGE); removableMediaView.display(thumbnail, mediaType == MediaType.IMAGE);
} }
@ -330,6 +341,8 @@ public class AttachmentManager {
return mediaType.createSlide(context, uri, fileName, mimeType, mediaSize, width, height); return mediaType.createSlide(context, uri, fileName, mimeType, mediaSize, width, height);
} }
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
return result;
} }
public boolean isAttachmentPresent() { public boolean isAttachmentPresent() {

View File

@ -16,6 +16,13 @@ public class SettableFuture<T> implements ListenableFuture<T> {
private volatile T result; private volatile T result;
private volatile Throwable exception; private volatile Throwable exception;
public SettableFuture() { }
public SettableFuture(T value) {
this.result = value;
this.completed = true;
}
@Override @Override
public synchronized boolean cancel(boolean mayInterruptIfRunning) { public synchronized boolean cancel(boolean mayInterruptIfRunning) {
if (!completed && !canceled) { if (!completed && !canceled) {
@ -64,6 +71,20 @@ public class SettableFuture<T> implements ListenableFuture<T> {
return true; return true;
} }
public void deferTo(ListenableFuture<T> other) {
other.addListener(new Listener<T>() {
@Override
public void onSuccess(T result) {
SettableFuture.this.set(result);
}
@Override
public void onFailure(ExecutionException e) {
SettableFuture.this.setException(e.getCause());
}
});
}
@Override @Override
public synchronized T get() throws InterruptedException, ExecutionException { public synchronized T get() throws InterruptedException, ExecutionException {
while (!completed) wait(); while (!completed) wait();