diff --git a/src/org/thoughtcrime/securesms/ConversationActivity.java b/src/org/thoughtcrime/securesms/ConversationActivity.java index 97b6d3fa36..699a469be1 100644 --- a/src/org/thoughtcrime/securesms/ConversationActivity.java +++ b/src/org/thoughtcrime/securesms/ConversationActivity.java @@ -180,6 +180,9 @@ import java.util.Collections; import java.util.LinkedList; import java.util.List; 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.database.GroupDatabase.GroupRecord; @@ -294,7 +297,18 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity @Override public void onSuccess(Boolean result) { initializeProfiles(); - initializeDraft(); + initializeDraft().addListener(new AssertedSuccessListener() { + @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 - private void initializeDraft() { + private ListenableFuture initializeDraft() { + final SettableFuture result = new SettableFuture<>(); + final String draftText = getIntent().getStringExtra(TEXT_EXTRA); final Uri draftMedia = getIntent().getData(); final MediaType draftMediaType = MediaType.from(getIntent().getType()); - - if (draftText != null) composeText.setText(draftText); - if (draftMedia != null && draftMediaType != null) setMedia(draftMedia, draftMediaType); + + if (draftText != null) { + composeText.setText(draftText); + result.set(true); + } + if (draftMedia != null && draftMediaType != null) { + return setMedia(draftMedia, draftMediaType); + } if (draftText == null && draftMedia == null && draftMediaType == null) { - initializeDraftFromDatabase(); + return initializeDraftFromDatabase(); } else { updateToggleButtonState(); + result.set(false); } + + return result; } private void initializeEnabledCheck() { @@ -1005,7 +1029,9 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity attachButton.setEnabled(enabled); } - private void initializeDraftFromDatabase() { + private ListenableFuture initializeDraftFromDatabase() { + SettableFuture future = new SettableFuture<>(); + new AsyncTask>() { @Override protected List doInBackground(Void... params) { @@ -1019,26 +1045,42 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity @Override protected void onPostExecute(List drafts) { + AtomicInteger draftsRemaining = new AtomicInteger(drafts.size()); + AtomicBoolean success = new AtomicBoolean(false); + ListenableFuture.Listener listener = new AssertedSuccessListener() { + @Override + public void onSuccess(Boolean result) { + success.compareAndSet(false, result); + + if (draftsRemaining.decrementAndGet() <= 0) { + future.set(success.get()); + } + } + }; + for (Draft draft : drafts) { try { switch (draft.getType()) { case Draft.TEXT: composeText.setText(draft.getValue()); + listener.onSuccess(true); break; case Draft.LOCATION: - attachmentManager.setLocation(SignalPlace.deserialize(draft.getValue()), getCurrentMediaConstraints()); + attachmentManager.setLocation(SignalPlace.deserialize(draft.getValue()), getCurrentMediaConstraints()).addListener(listener); break; case Draft.IMAGE: - setMedia(Uri.parse(draft.getValue()), MediaType.IMAGE); + setMedia(Uri.parse(draft.getValue()), MediaType.IMAGE).addListener(listener); break; case Draft.AUDIO: - setMedia(Uri.parse(draft.getValue()), MediaType.AUDIO); + setMedia(Uri.parse(draft.getValue()), MediaType.AUDIO).addListener(listener); break; case Draft.VIDEO: - setMedia(Uri.parse(draft.getValue()), MediaType.VIDEO); + setMedia(Uri.parse(draft.getValue()), MediaType.VIDEO).addListener(listener); break; case Draft.QUOTE: - new QuoteRestorationTask(draft.getValue()).execute(); + SettableFuture quoteResult = new SettableFuture<>(); + new QuoteRestorationTask(draft.getValue(), quoteResult).execute(); + quoteResult.addListener(listener); break; } } catch (IOException e) { @@ -1049,6 +1091,8 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity updateToggleButtonState(); } }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + + return future; } private ListenableFuture initializeSecurity(final boolean currentSecureText, @@ -1384,17 +1428,20 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity } } - private void setMedia(@Nullable Uri uri, @NonNull MediaType mediaType) { - setMedia(uri, mediaType, 0, 0); + private ListenableFuture setMedia(@Nullable Uri uri, @NonNull MediaType mediaType) { + return setMedia(uri, mediaType, 0, 0); } - private void setMedia(@Nullable Uri uri, @NonNull MediaType mediaType, int width, int height) { - if (uri == null) return; + private ListenableFuture setMedia(@Nullable Uri uri, @NonNull MediaType mediaType, int width, int height) { + if (uri == null) { + return new SettableFuture<>(false); + } if (MediaType.VCARD.equals(mediaType) && isSecureText) { openContactShareEditor(uri); + return new SettableFuture<>(false); } 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 { - private final String serialized; + private final String serialized; + private final SettableFuture future; - QuoteRestorationTask(@NonNull String serialized) { + QuoteRestorationTask(@NonNull String serialized, @NonNull SettableFuture future) { this.serialized = serialized; + this.future = future; } @Override @@ -2204,8 +2253,10 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity protected void onPostExecute(MessageRecord messageRecord) { if (messageRecord != null) { handleReplyMessage(messageRecord); + future.set(true); } else { Log.e(TAG, "Failed to restore a quote from a draft. No matching message record."); + future.set(false); } } } diff --git a/src/org/thoughtcrime/securesms/ConversationFragment.java b/src/org/thoughtcrime/securesms/ConversationFragment.java index 39d04862b5..20755c1eb3 100644 --- a/src/org/thoughtcrime/securesms/ConversationFragment.java +++ b/src/org/thoughtcrime/securesms/ConversationFragment.java @@ -190,6 +190,13 @@ public class ConversationFragment extends Fragment getLoaderManager().restartLoader(0, Bundle.EMPTY, this); } + public void moveToLastSeen() { + if (lastSeen > 0) { + int position = getListAdapter().findLastSeenPosition(lastSeen); + scrollToLastSeenPosition(position); + } + } + private void initializeResources() { this.recipient = Recipient.from(getActivity(), getActivity().getIntent().getParcelableExtra(ConversationActivity.ADDRESS_EXTRA), true); this.threadId = this.getActivity().getIntent().getLongExtra(ConversationActivity.THREAD_ID_EXTRA, -1); diff --git a/src/org/thoughtcrime/securesms/components/GlideListeningTarget.java b/src/org/thoughtcrime/securesms/components/GlideListeningTarget.java new file mode 100644 index 0000000000..c8b2e1e12f --- /dev/null +++ b/src/org/thoughtcrime/securesms/components/GlideListeningTarget.java @@ -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 loaded; + + public GlideListeningTarget(@NonNull ImageView view, @NonNull SettableFuture 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); + } +} diff --git a/src/org/thoughtcrime/securesms/components/ThumbnailView.java b/src/org/thoughtcrime/securesms/components/ThumbnailView.java index 6c8de12cb6..00fffdcd1f 100644 --- a/src/org/thoughtcrime/securesms/components/ThumbnailView.java +++ b/src/org/thoughtcrime/securesms/components/ThumbnailView.java @@ -5,6 +5,7 @@ import android.content.res.TypedArray; import android.net.Uri; import android.support.annotation.DrawableRes; import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.support.annotation.UiThread; import android.util.AttributeSet; import android.util.Log; @@ -14,12 +15,16 @@ import android.widget.FrameLayout; import android.widget.ImageView; import com.bumptech.glide.RequestBuilder; +import com.bumptech.glide.load.DataSource; 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.CenterCrop; import com.bumptech.glide.load.resource.bitmap.FitCenter; 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.target.Target; import org.thoughtcrime.securesms.R; 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.util.Util; 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 java.util.Locale; @@ -221,16 +228,16 @@ public class ThumbnailView extends FrameLayout { } @UiThread - public void setImageResource(@NonNull GlideRequests glideRequests, @NonNull Slide slide, - boolean showControls, boolean isPreview) + public ListenableFuture setImageResource(@NonNull GlideRequests glideRequests, @NonNull Slide slide, + boolean showControls, boolean isPreview) { - setImageResource(glideRequests, slide, showControls, isPreview, 0, 0); + return setImageResource(glideRequests, slide, showControls, isPreview, 0, 0); } @UiThread - public void setImageResource(@NonNull GlideRequests glideRequests, @NonNull Slide slide, - boolean showControls, boolean isPreview, int naturalWidth, - int naturalHeight) + public ListenableFuture setImageResource(@NonNull GlideRequests glideRequests, @NonNull Slide slide, + boolean showControls, boolean isPreview, int naturalWidth, + int naturalHeight) { if (showControls) { getTransferControls().setSlide(slide); @@ -249,7 +256,7 @@ public class ThumbnailView extends FrameLayout { if (Util.equals(slide, this.slide)) { Log.w(TAG, "Not re-loading slide " + slide.asAttachment().getDataUri()); - return; + return new SettableFuture<>(false); } 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()); this.slide = slide; - return; + return new SettableFuture<>(false); } Log.w(TAG, "loading part with id " + slide.asAttachment().getDataUri() @@ -270,19 +277,31 @@ public class ThumbnailView extends FrameLayout { dimens[HEIGHT] = naturalHeight; invalidate(); - if (slide.getThumbnailUri() != null) buildThumbnailGlideRequest(glideRequests, slide).into(image); - else if (slide.hasPlaceholder()) buildPlaceholderGlideRequest(glideRequests, slide).into(image); - else glideRequests.clear(image); + SettableFuture result = new SettableFuture<>(); + 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 setImageResource(@NonNull GlideRequests glideRequests, @NonNull Uri uri) { + SettableFuture future = new SettableFuture<>(); + if (transferControls.isPresent()) getTransferControls().setVisibility(View.GONE); glideRequests.load(new DecryptableUri(uri)) .diskCacheStrategy(DiskCacheStrategy.NONE) .transforms(new CenterCrop(), new RoundedCorners(radius)) .transition(withCrossFade()) - .into(image); + .into(new GlideListeningTarget(image, future)); + + return future; } public void setThumbnailClickListener(SlideClickListener listener) { diff --git a/src/org/thoughtcrime/securesms/mms/AttachmentManager.java b/src/org/thoughtcrime/securesms/mms/AttachmentManager.java index 9268075cb0..f2adfd29a6 100644 --- a/src/org/thoughtcrime/securesms/mms/AttachmentManager.java +++ b/src/org/thoughtcrime/securesms/mms/AttachmentManager.java @@ -63,6 +63,7 @@ import org.thoughtcrime.securesms.util.ViewUtil; import org.thoughtcrime.securesms.util.concurrent.AssertedSuccessListener; import org.thoughtcrime.securesms.util.concurrent.ListenableFuture; import org.thoughtcrime.securesms.util.concurrent.ListenableFuture.Listener; +import org.thoughtcrime.securesms.util.concurrent.SettableFuture; import org.thoughtcrime.securesms.util.views.Stub; import org.whispersystems.libsignal.util.guava.Optional; @@ -181,12 +182,13 @@ public class AttachmentManager { this.slide = Optional.of(slide); } - public void setLocation(@NonNull final SignalPlace place, - @NonNull final MediaConstraints constraints) + public ListenableFuture setLocation(@NonNull final SignalPlace place, + @NonNull final MediaConstraints constraints) { inflateStub(); - ListenableFuture future = mapView.display(place); + SettableFuture returnResult = new SettableFuture<>(); + ListenableFuture future = mapView.display(place); attachmentViewStub.get().setVisibility(View.VISIBLE); removableMediaView.display(mapView, false); @@ -201,20 +203,25 @@ public class AttachmentManager { setSlide(locationSlide); attachmentListener.onAttachmentChanged(); + returnResult.set(true); } }); + + return returnResult; } @SuppressLint("StaticFieldLeak") - public void setMedia(@NonNull final GlideRequests glideRequests, - @NonNull final Uri uri, - @NonNull final MediaType mediaType, - @NonNull final MediaConstraints constraints, - final int width, - final int height) + public ListenableFuture setMedia(@NonNull final GlideRequests glideRequests, + @NonNull final Uri uri, + @NonNull final MediaType mediaType, + @NonNull final MediaConstraints constraints, + final int width, + final int height) { inflateStub(); + final SettableFuture result = new SettableFuture<>(); + new AsyncTask() { @Override protected void onPreExecute() { @@ -247,11 +254,13 @@ public class AttachmentManager { Toast.makeText(context, R.string.ConversationActivity_sorry_there_was_an_error_setting_your_attachment, Toast.LENGTH_SHORT).show(); + result.set(false); } else if (!areConstraintsSatisfied(context, slide, constraints)) { attachmentViewStub.get().setVisibility(View.GONE); Toast.makeText(context, R.string.ConversationActivity_attachment_exceeds_size_limits, Toast.LENGTH_SHORT).show(); + result.set(false); } else { setSlide(slide); attachmentViewStub.get().setVisibility(View.VISIBLE); @@ -259,12 +268,14 @@ public class AttachmentManager { if (slide.hasAudio()) { audioView.setAudio((AudioSlide) slide, false); removableMediaView.display(audioView, false); + result.set(true); } else if (slide.hasDocument()) { documentView.setDocument((DocumentSlide) slide, false); removableMediaView.display(documentView, false); + result.set(true); } else { 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); } @@ -330,6 +341,8 @@ public class AttachmentManager { return mediaType.createSlide(context, uri, fileName, mimeType, mediaSize, width, height); } }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + + return result; } public boolean isAttachmentPresent() { diff --git a/src/org/thoughtcrime/securesms/util/concurrent/SettableFuture.java b/src/org/thoughtcrime/securesms/util/concurrent/SettableFuture.java index 8f0c2436a2..80fd166c4b 100644 --- a/src/org/thoughtcrime/securesms/util/concurrent/SettableFuture.java +++ b/src/org/thoughtcrime/securesms/util/concurrent/SettableFuture.java @@ -16,6 +16,13 @@ public class SettableFuture implements ListenableFuture { private volatile T result; private volatile Throwable exception; + public SettableFuture() { } + + public SettableFuture(T value) { + this.result = value; + this.completed = true; + } + @Override public synchronized boolean cancel(boolean mayInterruptIfRunning) { if (!completed && !canceled) { @@ -64,6 +71,20 @@ public class SettableFuture implements ListenableFuture { return true; } + public void deferTo(ListenableFuture other) { + other.addListener(new Listener() { + @Override + public void onSuccess(T result) { + SettableFuture.this.set(result); + } + + @Override + public void onFailure(ExecutionException e) { + SettableFuture.this.setException(e.getCause()); + } + }); + } + @Override public synchronized T get() throws InterruptedException, ExecutionException { while (!completed) wait();