diff --git a/res/layout/media_preview_activity.xml b/res/layout/media_preview_activity.xml
index 3d88a165ea..b9128ce4a0 100644
--- a/res/layout/media_preview_activity.xml
+++ b/res/layout/media_preview_activity.xml
@@ -1,20 +1,13 @@
-
-
-
-
+ android:layout_height="match_parent"/>
-
+
diff --git a/res/layout/media_view.xml b/res/layout/media_view.xml
new file mode 100644
index 0000000000..c4ef9fcaa4
--- /dev/null
+++ b/res/layout/media_view.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/res/layout/media_view_page.xml b/res/layout/media_view_page.xml
new file mode 100644
index 0000000000..58e1386a8d
--- /dev/null
+++ b/res/layout/media_view_page.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/org/thoughtcrime/securesms/ConversationFragment.java b/src/org/thoughtcrime/securesms/ConversationFragment.java
index 24b04bd7b7..0a1b31f7b9 100644
--- a/src/org/thoughtcrime/securesms/ConversationFragment.java
+++ b/src/org/thoughtcrime/securesms/ConversationFragment.java
@@ -391,7 +391,7 @@ public class ConversationFragment extends Fragment
public void onClick(DialogInterface dialog, int which) {
for (Slide slide : message.getSlideDeck().getSlides()) {
if ((slide.hasImage() || slide.hasVideo() || slide.hasAudio() || slide.hasDocument()) && slide.getUri() != null) {
- SaveAttachmentTask saveTask = new SaveAttachmentTask(getActivity(), masterSecret, list);
+ SaveAttachmentTask saveTask = new SaveAttachmentTask(getActivity(), masterSecret);
saveTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, new Attachment(slide.getUri(), slide.getContentType(), message.getDateReceived(), slide.getFileName().orNull()));
return;
}
diff --git a/src/org/thoughtcrime/securesms/MediaPreviewActivity.java b/src/org/thoughtcrime/securesms/MediaPreviewActivity.java
index 6489b73e32..201e4975e4 100644
--- a/src/org/thoughtcrime/securesms/MediaPreviewActivity.java
+++ b/src/org/thoughtcrime/securesms/MediaPreviewActivity.java
@@ -17,27 +17,43 @@
package org.thoughtcrime.securesms;
import android.Manifest;
+import android.annotation.SuppressLint;
import android.annotation.TargetApi;
+import android.content.Context;
import android.content.Intent;
+import android.database.Cursor;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES;
import android.os.Bundle;
import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.v4.app.LoaderManager;
+import android.support.v4.content.Loader;
+import android.support.v4.util.Pair;
+import android.support.v4.view.PagerAdapter;
+import android.support.v4.view.ViewPager;
import android.util.Log;
+import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
+import android.view.ViewGroup;
+import android.view.Window;
import android.view.WindowManager;
+import android.widget.FrameLayout;
import android.widget.Toast;
-import org.thoughtcrime.securesms.components.ZoomingImageView;
+import org.thoughtcrime.securesms.components.MediaView;
+import org.thoughtcrime.securesms.components.viewpager.ExtendedOnPageChangedListener;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.database.Address;
+import org.thoughtcrime.securesms.database.MediaDatabase.MediaRecord;
+import org.thoughtcrime.securesms.database.loaders.PagingMediaLoader;
import org.thoughtcrime.securesms.mms.GlideApp;
-import org.thoughtcrime.securesms.mms.VideoSlide;
+import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientModifiedListener;
@@ -46,14 +62,15 @@ import org.thoughtcrime.securesms.util.DynamicLanguage;
import org.thoughtcrime.securesms.util.SaveAttachmentTask;
import org.thoughtcrime.securesms.util.SaveAttachmentTask.Attachment;
import org.thoughtcrime.securesms.util.Util;
-import org.thoughtcrime.securesms.video.VideoPlayer;
import java.io.IOException;
+import java.util.WeakHashMap;
/**
* Activity for displaying media attachments in-app
*/
-public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity implements RecipientModifiedListener {
+public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity implements RecipientModifiedListener, LoaderManager.LoaderCallbacks> {
+
private final static String TAG = MediaPreviewActivity.class.getSimpleName();
public static final String ADDRESS_EXTRA = "address";
@@ -65,16 +82,13 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
private MasterSecret masterSecret;
- private ZoomingImageView image;
- private VideoPlayer video;
-
- private Uri mediaUri;
- private String mediaType;
- private Recipient recipient;
- private long date;
- private long size;
- private boolean outgoing;
+ private ViewPager mediaPager;
+ private Uri initialMediaUri;
+ private String initialMediaType;
+ private long initialMediaSize;
+ private Recipient conversationRecipient;
+ @SuppressWarnings("ConstantConditions")
@Override
protected void onCreate(Bundle bundle, @NonNull MasterSecret masterSecret) {
this.masterSecret = masterSecret;
@@ -90,7 +104,6 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
initializeViews();
initializeResources();
- initializeActionBar();
}
@Override
@@ -110,126 +123,126 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
Util.runOnMain(this::initializeActionBar);
}
+ @SuppressWarnings("ConstantConditions")
private void initializeActionBar() {
- final CharSequence relativeTimeSpan;
+ MediaItem mediaItem = getCurrentMediaItem();
- if (date > 0) {
- relativeTimeSpan = DateUtils.getExtendedRelativeTimeSpanString(this,dynamicLanguage.getCurrentLocale(),date);
- } else {
- relativeTimeSpan = getString(R.string.MediaPreviewActivity_draft);
- }
+ if (mediaItem != null) {
+ CharSequence relativeTimeSpan;
- if (outgoing) getSupportActionBar().setTitle(getString(R.string.MediaPreviewActivity_you));
- else getSupportActionBar().setTitle(recipient.toShortString());
+ if (mediaItem.date > 0) {
+ relativeTimeSpan = DateUtils.getExtendedRelativeTimeSpanString(this,dynamicLanguage.getCurrentLocale(), mediaItem.date);
+ } else {
+ relativeTimeSpan = getString(R.string.MediaPreviewActivity_draft);
+ }
+
+ if (mediaItem.outgoing) getSupportActionBar().setTitle(getString(R.string.MediaPreviewActivity_you));
+ else if (mediaItem.recipient != null) getSupportActionBar().setTitle(mediaItem.recipient.toShortString());
+ else getSupportActionBar().setTitle("");
- getSupportActionBar().setSubtitle(relativeTimeSpan);
+ getSupportActionBar().setSubtitle(relativeTimeSpan);
+ }
}
@Override
public void onResume() {
super.onResume();
+
dynamicLanguage.onResume(this);
- if (recipient != null) recipient.addListener(this);
initializeMedia();
}
@Override
public void onPause() {
super.onPause();
- if (recipient != null) recipient.removeListener(this);
cleanupMedia();
}
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
- if (recipient != null) recipient.removeListener(this);
setIntent(intent);
initializeResources();
- initializeActionBar();
}
private void initializeViews() {
- image = findViewById(R.id.image);
- video = findViewById(R.id.video_player);
+ mediaPager = findViewById(R.id.media_pager);
+ mediaPager.setOffscreenPageLimit(1);
+ mediaPager.addOnPageChangeListener(new ViewPagerListener());
}
private void initializeResources() {
Address address = getIntent().getParcelableExtra(ADDRESS_EXTRA);
- mediaUri = getIntent().getData();
- mediaType = getIntent().getType();
- date = getIntent().getLongExtra(DATE_EXTRA, -1);
- size = getIntent().getLongExtra(SIZE_EXTRA, 0);
- outgoing = getIntent().getBooleanExtra(OUTGOING_EXTRA, false);
+ initialMediaUri = getIntent().getData();
+ initialMediaType = getIntent().getType();
+ initialMediaSize = getIntent().getLongExtra(SIZE_EXTRA, 0);
if (address != null) {
- recipient = Recipient.from(this, address, true);
- recipient.addListener(this);
+ conversationRecipient = Recipient.from(this, address, true);
} else {
- recipient = null;
+ conversationRecipient = null;
}
}
private void initializeMedia() {
- if (!isContentTypeSupported(mediaType)) {
+ if (!isContentTypeSupported(initialMediaType)) {
Log.w(TAG, "Unsupported media type sent to MediaPreviewActivity, finishing.");
Toast.makeText(getApplicationContext(), R.string.MediaPreviewActivity_unssuported_media_type, Toast.LENGTH_LONG).show();
finish();
}
- Log.w(TAG, "Loading Part URI: " + mediaUri);
-
- try {
- if (mediaType != null && mediaType.startsWith("image/")) {
- image.setVisibility(View.VISIBLE);
- video.setVisibility(View.GONE);
- image.setImageUri(masterSecret, GlideApp.with(this), mediaUri, mediaType);
- } else if (mediaType != null && mediaType.startsWith("video/")) {
- image.setVisibility(View.GONE);
- video.setVisibility(View.VISIBLE);
- video.setWindow(getWindow());
- video.setVideoSource(masterSecret, new VideoSlide(this, mediaUri, size));
- }
- } catch (IOException e) {
- Log.w(TAG, e);
- Toast.makeText(getApplicationContext(), R.string.MediaPreviewActivity_unssuported_media_type, Toast.LENGTH_LONG).show();
- finish();
+ Log.w(TAG, "Loading Part URI: " + initialMediaUri);
+
+ if (conversationRecipient != null) {
+ getSupportLoaderManager().initLoader(0, null, this);
+ } else {
+ mediaPager.setAdapter(new SingleItemPagerAdapter(this, masterSecret, GlideApp.with(this), getWindow(), initialMediaUri, initialMediaType, initialMediaSize));
}
}
private void cleanupMedia() {
- image.cleanup();
- video.cleanup();
+ mediaPager.removeAllViews();
+ mediaPager.setAdapter(null);
}
private void showOverview() {
Intent intent = new Intent(this, MediaOverviewActivity.class);
- intent.putExtra(MediaOverviewActivity.ADDRESS_EXTRA, recipient.getAddress());
+ intent.putExtra(MediaOverviewActivity.ADDRESS_EXTRA, conversationRecipient.getAddress());
startActivity(intent);
}
private void forward() {
- Intent composeIntent = new Intent(this, ShareActivity.class);
- composeIntent.putExtra(Intent.EXTRA_STREAM, mediaUri);
- composeIntent.setType(mediaType);
- startActivity(composeIntent);
+ MediaItem mediaItem = getCurrentMediaItem();
+
+ if (mediaItem != null) {
+ Intent composeIntent = new Intent(this, ShareActivity.class);
+ composeIntent.putExtra(Intent.EXTRA_STREAM, mediaItem.uri);
+ composeIntent.setType(mediaItem.type);
+ startActivity(composeIntent);
+ }
}
+ @SuppressWarnings("CodeBlock2Expr")
+ @SuppressLint("InlinedApi")
private void saveToDisk() {
- SaveAttachmentTask.showWarningDialog(this, (dialogInterface, i) -> {
- Permissions.with(this)
- .request(Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE)
- .ifNecessary()
- .withPermanentDenialDialog(getString(R.string.MediaPreviewActivity_signal_needs_the_storage_permission_in_order_to_write_to_external_storage_but_it_has_been_permanently_denied))
- .onAnyDenied(() -> Toast.makeText(this, R.string.MediaPreviewActivity_unable_to_write_to_external_storage_without_permission, Toast.LENGTH_LONG).show())
- .onAllGranted(() -> {
- SaveAttachmentTask saveTask = new SaveAttachmentTask(MediaPreviewActivity.this, masterSecret, image);
- long saveDate = (date > 0) ? date : System.currentTimeMillis();
- saveTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, new Attachment(mediaUri, mediaType, saveDate, null));
- })
- .execute();
- });
+ MediaItem mediaItem = getCurrentMediaItem();
+
+ if (mediaItem != null) {
+ SaveAttachmentTask.showWarningDialog(this, (dialogInterface, i) -> {
+ Permissions.with(this)
+ .request(Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE)
+ .ifNecessary()
+ .withPermanentDenialDialog(getString(R.string.MediaPreviewActivity_signal_needs_the_storage_permission_in_order_to_write_to_external_storage_but_it_has_been_permanently_denied))
+ .onAnyDenied(() -> Toast.makeText(this, R.string.MediaPreviewActivity_unable_to_write_to_external_storage_without_permission, Toast.LENGTH_LONG).show())
+ .onAllGranted(() -> {
+ SaveAttachmentTask saveTask = new SaveAttachmentTask(MediaPreviewActivity.this, masterSecret);
+ long saveDate = (mediaItem.date > 0) ? mediaItem.date : System.currentTimeMillis();
+ saveTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, new Attachment(mediaItem.uri, mediaItem.type, saveDate, null));
+ })
+ .execute();
+ });
+ }
}
@Override
@@ -239,7 +252,7 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
menu.clear();
MenuInflater inflater = this.getMenuInflater();
inflater.inflate(R.menu.media_preview, menu);
- if (recipient == null) menu.findItem(R.id.media_preview__overview).setVisible(false);
+ if (conversationRecipient == null) menu.findItem(R.id.media_preview__overview).setVisible(false);
return true;
}
@@ -258,7 +271,260 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
return false;
}
+ private @Nullable MediaItem getCurrentMediaItem() {
+ MediaItemAdapter adapter = (MediaItemAdapter)mediaPager.getAdapter();
+
+ if (adapter != null) {
+ return adapter.getMediaItemFor(mediaPager.getCurrentItem());
+ } else {
+ return null;
+ }
+ }
+
public static boolean isContentTypeSupported(final String contentType) {
return contentType != null && (contentType.startsWith("image/") || contentType.startsWith("video/"));
}
+
+ @Override
+ public Loader> onCreateLoader(int id, Bundle args) {
+ return new PagingMediaLoader(this, conversationRecipient, initialMediaUri);
+ }
+
+ @Override
+ public void onLoadFinished(Loader> loader, @Nullable Pair data) {
+ if (data != null) {
+ @SuppressWarnings("ConstantConditions")
+ CursorPagerAdapter adapter = new CursorPagerAdapter(this, masterSecret, GlideApp.with(this), getWindow(), data.first, data.second);
+ mediaPager.setAdapter(adapter);
+ adapter.setActive(true);
+ mediaPager.setCurrentItem(data.second);
+ }
+ }
+
+ @Override
+ public void onLoaderReset(Loader> loader) {
+
+ }
+
+ private class ViewPagerListener extends ExtendedOnPageChangedListener {
+
+ @Override
+ public void onPageSelected(int position) {
+ super.onPageSelected(position);
+
+ MediaItemAdapter adapter = (MediaItemAdapter)mediaPager.getAdapter();
+
+ if (adapter != null) {
+ MediaItem item = adapter.getMediaItemFor(position);
+ if (item.recipient != null) item.recipient.addListener(MediaPreviewActivity.this);
+
+ initializeActionBar();
+ }
+ }
+
+
+ @Override
+ public void onPageUnselected(int position) {
+ MediaItemAdapter adapter = (MediaItemAdapter)mediaPager.getAdapter();
+
+ if (adapter != null) {
+ MediaItem item = adapter.getMediaItemFor(position);
+ if (item.recipient != null) item.recipient.removeListener(MediaPreviewActivity.this);
+
+ adapter.pause(position);
+ }
+ }
+ }
+
+ private static class SingleItemPagerAdapter extends PagerAdapter implements MediaItemAdapter {
+
+ private final MasterSecret masterSecret;
+ private final GlideRequests glideRequests;
+ private final Window window;
+ private final Uri uri;
+ private final String mediaType;
+ private final long size;
+
+ private final LayoutInflater inflater;
+
+ SingleItemPagerAdapter(@NonNull Context context, @NonNull MasterSecret masterSecret,
+ @NonNull GlideRequests glideRequests, @NonNull Window window,
+ @NonNull Uri uri, @NonNull String mediaType, long size)
+ {
+ this.masterSecret = masterSecret;
+ this.glideRequests = glideRequests;
+ this.window = window;
+ this.uri = uri;
+ this.mediaType = mediaType;
+ this.size = size;
+ this.inflater = LayoutInflater.from(context);
+ }
+
+ @Override
+ public int getCount() {
+ return 1;
+ }
+
+ @Override
+ public boolean isViewFromObject(@NonNull View view, @NonNull Object object) {
+ return view == object;
+ }
+
+ @Override
+ public @NonNull Object instantiateItem(@NonNull ViewGroup container, int position) {
+ View itemView = inflater.inflate(R.layout.media_view_page, container, false);
+ MediaView mediaView = itemView.findViewById(R.id.media_view);
+
+ try {
+ mediaView.set(masterSecret, glideRequests, window, uri, mediaType, size, true);
+ } catch (IOException e) {
+ Log.w(TAG, e);
+ }
+
+ container.addView(itemView);
+
+ return itemView;
+ }
+
+ @Override
+ public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
+ MediaView mediaView = ((FrameLayout)object).findViewById(R.id.media_view);
+ mediaView.cleanup();
+
+ container.removeView((FrameLayout)object);
+ }
+
+ @Override
+ public MediaItem getMediaItemFor(int position) {
+ return new MediaItem(null, uri, mediaType, -1, true);
+ }
+
+ @Override
+ public void pause(int position) {
+
+ }
+ }
+
+ private static class CursorPagerAdapter extends PagerAdapter implements MediaItemAdapter {
+
+ private final WeakHashMap mediaViews = new WeakHashMap<>();
+
+ private final Context context;
+ private final MasterSecret masterSecret;
+ private final GlideRequests glideRequests;
+ private final Window window;
+ private final Cursor cursor;
+
+ private boolean active;
+ private int autoPlayPosition;
+
+ CursorPagerAdapter(@NonNull Context context, @NonNull MasterSecret masterSecret,
+ @NonNull GlideRequests glideRequests, @NonNull Window window,
+ @NonNull Cursor cursor, int autoPlayPosition)
+ {
+ this.context = context.getApplicationContext();
+ this.masterSecret = masterSecret;
+ this.glideRequests = glideRequests;
+ this.window = window;
+ this.cursor = cursor;
+ this.autoPlayPosition = autoPlayPosition;
+ }
+
+ public void setActive(boolean active) {
+ this.active = active;
+ notifyDataSetChanged();
+ }
+
+ @Override
+ public int getCount() {
+ if (!active) return 0;
+ else return cursor.getCount();
+ }
+
+ @Override
+ public boolean isViewFromObject(@NonNull View view, @NonNull Object object) {
+ return view == object;
+ }
+
+ @Override
+ public @NonNull Object instantiateItem(@NonNull ViewGroup container, int position) {
+ View itemView = LayoutInflater.from(context).inflate(R.layout.media_view_page, container, false);
+ MediaView mediaView = itemView.findViewById(R.id.media_view);
+ boolean autoplay = position == autoPlayPosition;
+ int cursorPosition = getCursorPosition(position);
+
+ autoPlayPosition = -1;
+
+ cursor.moveToPosition(cursorPosition);
+
+ MediaRecord mediaRecord = MediaRecord.from(context, masterSecret, cursor);
+
+ try {
+ //noinspection ConstantConditions
+ mediaView.set(masterSecret, glideRequests, window, mediaRecord.getAttachment().getDataUri(), mediaRecord.getAttachment().getContentType(), mediaRecord.getAttachment().getSize(), autoplay);
+ } catch (IOException e) {
+ Log.w(TAG, e);
+ }
+
+ mediaViews.put(position, mediaView);
+ container.addView(itemView);
+
+ return itemView;
+ }
+
+ @Override
+ public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
+ MediaView mediaView = ((FrameLayout)object).findViewById(R.id.media_view);
+ mediaView.cleanup();
+
+ mediaViews.remove(position);
+ container.removeView((FrameLayout)object);
+ }
+
+ public MediaItem getMediaItemFor(int position) {
+ cursor.moveToPosition(getCursorPosition(position));
+ MediaRecord mediaRecord = MediaRecord.from(context, masterSecret, cursor);
+ Address address = mediaRecord.getAddress();
+
+ if (mediaRecord.getAttachment().getDataUri() == null) throw new AssertionError();
+
+ return new MediaItem(address != null ? Recipient.from(context, address,true) : null,
+ mediaRecord.getAttachment().getDataUri(),
+ mediaRecord.getContentType(),
+ mediaRecord.getDate(),
+ mediaRecord.isOutgoing());
+ }
+
+ @Override
+ public void pause(int position) {
+ MediaView mediaView = mediaViews.get(position);
+ if (mediaView != null) mediaView.pause();
+ }
+
+ private int getCursorPosition(int position) {
+ return cursor.getCount() - 1 - position;
+ }
+ }
+
+ private static class MediaItem {
+ private final @Nullable Recipient recipient;
+ private final @NonNull Uri uri;
+ private final @NonNull String type;
+ private final long date;
+ private final boolean outgoing;
+
+ private MediaItem(@Nullable Recipient recipient, @NonNull Uri uri, @NonNull String type, long date, boolean outgoing) {
+ this.recipient = recipient;
+ this.uri = uri;
+ this.type = type;
+ this.date = date;
+ this.outgoing = outgoing;
+ }
+ }
+
+ interface MediaItemAdapter {
+ MediaItem getMediaItemFor(int position);
+ void pause(int position);
+ }
+
}
diff --git a/src/org/thoughtcrime/securesms/components/MediaView.java b/src/org/thoughtcrime/securesms/components/MediaView.java
new file mode 100644
index 0000000000..17667a4eb0
--- /dev/null
+++ b/src/org/thoughtcrime/securesms/components/MediaView.java
@@ -0,0 +1,87 @@
+package org.thoughtcrime.securesms.components;
+
+
+import android.content.Context;
+import android.net.Uri;
+import android.os.Build;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.annotation.RequiresApi;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.Window;
+import android.widget.FrameLayout;
+
+import org.thoughtcrime.securesms.R;
+import org.thoughtcrime.securesms.crypto.MasterSecret;
+import org.thoughtcrime.securesms.mms.GlideRequests;
+import org.thoughtcrime.securesms.mms.VideoSlide;
+import org.thoughtcrime.securesms.video.VideoPlayer;
+
+import java.io.IOException;
+
+public class MediaView extends FrameLayout {
+
+ private ZoomingImageView imageView;
+ private VideoPlayer videoView;
+
+ public MediaView(@NonNull Context context) {
+ super(context);
+ initialize();
+ }
+
+ public MediaView(@NonNull Context context, @Nullable AttributeSet attrs) {
+ super(context, attrs);
+ initialize();
+ }
+
+ public MediaView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ initialize();
+ }
+
+ @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
+ public MediaView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ initialize();
+ }
+
+ private void initialize() {
+ inflate(getContext(), R.layout.media_view, this);
+
+ this.imageView = findViewById(R.id.image);
+ this.videoView = findViewById(R.id.video_player);
+ }
+
+ public void set(@NonNull MasterSecret masterSecret,
+ @NonNull GlideRequests glideRequests,
+ @NonNull Window window,
+ @NonNull Uri source,
+ @NonNull String mediaType,
+ long size,
+ boolean autoplay)
+ throws IOException
+ {
+ if (mediaType.startsWith("image/")) {
+ imageView.setVisibility(View.VISIBLE);
+ videoView.setVisibility(View.GONE);
+ imageView.setImageUri(masterSecret, glideRequests, source, mediaType);
+ } else if (mediaType.startsWith("video/")) {
+ imageView.setVisibility(View.GONE);
+ videoView.setVisibility(View.VISIBLE);
+ videoView.setWindow(window);
+ videoView.setVideoSource(masterSecret, new VideoSlide(getContext(), source, size), autoplay);
+ } else {
+ throw new IOException("Unsupported media type: " + mediaType);
+ }
+ }
+
+ public void pause() {
+ this.videoView.pause();
+ }
+
+ public void cleanup() {
+ this.imageView.cleanup();
+ this.videoView.cleanup();
+ }
+}
diff --git a/src/org/thoughtcrime/securesms/components/viewpager/ExtendedOnPageChangedListener.java b/src/org/thoughtcrime/securesms/components/viewpager/ExtendedOnPageChangedListener.java
new file mode 100644
index 0000000000..62d0390102
--- /dev/null
+++ b/src/org/thoughtcrime/securesms/components/viewpager/ExtendedOnPageChangedListener.java
@@ -0,0 +1,29 @@
+package org.thoughtcrime.securesms.components.viewpager;
+
+
+import android.support.v4.view.ViewPager;
+
+public abstract class ExtendedOnPageChangedListener implements ViewPager.OnPageChangeListener {
+
+ private Integer currentPage = null;
+
+ @Override
+ public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
+
+ }
+
+ @Override
+ public void onPageSelected(int position) {
+ if (currentPage != null && currentPage != position) onPageUnselected(currentPage);
+ currentPage = position;
+ }
+
+ public abstract void onPageUnselected(int position);
+
+ @Override
+ public void onPageScrollStateChanged(int state) {
+
+ }
+
+
+}
diff --git a/src/org/thoughtcrime/securesms/database/AttachmentDatabase.java b/src/org/thoughtcrime/securesms/database/AttachmentDatabase.java
index 479aa9ab96..5322e91461 100644
--- a/src/org/thoughtcrime/securesms/database/AttachmentDatabase.java
+++ b/src/org/thoughtcrime/securesms/database/AttachmentDatabase.java
@@ -67,7 +67,7 @@ public class AttachmentDatabase extends Database {
static final String TABLE_NAME = "part";
static final String ROW_ID = "_id";
- static final String ATTACHMENT_ID_ALIAS = "attachment_id";
+ public static final String ATTACHMENT_ID_ALIAS = "attachment_id";
static final String MMS_ID = "mid";
static final String CONTENT_TYPE = "ct";
static final String NAME = "name";
@@ -79,7 +79,7 @@ public class AttachmentDatabase extends Database {
static final String FILE_NAME = "file_name";
static final String THUMBNAIL = "thumbnail";
static final String THUMBNAIL_ASPECT_RATIO = "aspect_ratio";
- static final String UNIQUE_ID = "unique_id";
+ public static final String UNIQUE_ID = "unique_id";
static final String DIGEST = "digest";
static final String VOICE_NOTE = "voice_note";
public static final String FAST_PREFLIGHT_ID = "fast_preflight_id";
diff --git a/src/org/thoughtcrime/securesms/database/loaders/PagingMediaLoader.java b/src/org/thoughtcrime/securesms/database/loaders/PagingMediaLoader.java
new file mode 100644
index 0000000000..d11ca8273d
--- /dev/null
+++ b/src/org/thoughtcrime/securesms/database/loaders/PagingMediaLoader.java
@@ -0,0 +1,49 @@
+package org.thoughtcrime.securesms.database.loaders;
+
+
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.v4.util.Pair;
+
+import org.thoughtcrime.securesms.attachments.AttachmentId;
+import org.thoughtcrime.securesms.database.AttachmentDatabase;
+import org.thoughtcrime.securesms.database.DatabaseFactory;
+import org.thoughtcrime.securesms.mms.PartAuthority;
+import org.thoughtcrime.securesms.recipients.Recipient;
+import org.thoughtcrime.securesms.util.AsyncLoader;
+
+public class PagingMediaLoader extends AsyncLoader> {
+
+ @SuppressWarnings("unused")
+ private static final String TAG = PagingMediaLoader.class.getSimpleName();
+
+ private final Recipient recipient;
+ private final Uri uri;
+
+ public PagingMediaLoader(@NonNull Context context, @NonNull Recipient recipient, @NonNull Uri uri) {
+ super(context);
+ this.recipient = recipient;
+ this.uri = uri;
+ }
+
+ @Nullable
+ @Override
+ public Pair loadInBackground() {
+ long threadId = DatabaseFactory.getThreadDatabase(getContext()).getThreadIdFor(recipient);
+ Cursor cursor = DatabaseFactory.getMediaDatabase(getContext()).getGalleryMediaForThread(threadId);
+
+ while (cursor != null && cursor.moveToNext()) {
+ AttachmentId attachmentId = new AttachmentId(cursor.getLong(cursor.getColumnIndexOrThrow(AttachmentDatabase.ATTACHMENT_ID_ALIAS)), cursor.getLong(cursor.getColumnIndexOrThrow(AttachmentDatabase.UNIQUE_ID)));
+ Uri attachmentUri = PartAuthority.getAttachmentDataUri(attachmentId);
+
+ if (attachmentUri.equals(uri)) {
+ return new Pair<>(cursor, cursor.getCount() - 1 - cursor.getPosition());
+ }
+ }
+
+ return null;
+ }
+}
diff --git a/src/org/thoughtcrime/securesms/util/AbstractCursorLoader.java b/src/org/thoughtcrime/securesms/util/AbstractCursorLoader.java
index 00afc8ca42..1e0e550c8f 100644
--- a/src/org/thoughtcrime/securesms/util/AbstractCursorLoader.java
+++ b/src/org/thoughtcrime/securesms/util/AbstractCursorLoader.java
@@ -1,5 +1,6 @@
package org.thoughtcrime.securesms.util;
+import android.annotation.SuppressLint;
import android.content.Context;
import android.database.Cursor;
import android.support.v4.content.AsyncTaskLoader;
@@ -9,10 +10,13 @@ import android.support.v4.content.AsyncTaskLoader;
* to get the benefits of reloading when content has changed.
*/
public abstract class AbstractCursorLoader extends AsyncTaskLoader {
+
+ @SuppressWarnings("unused")
private static final String TAG = AbstractCursorLoader.class.getSimpleName();
- protected final ForceLoadContentObserver observer;
+ @SuppressLint("StaticFieldLeak")
protected final Context context;
+ private final ForceLoadContentObserver observer;
protected Cursor cursor;
public AbstractCursorLoader(Context context) {
diff --git a/src/org/thoughtcrime/securesms/util/SaveAttachmentTask.java b/src/org/thoughtcrime/securesms/util/SaveAttachmentTask.java
index 0e2641950a..bb9caf99f3 100644
--- a/src/org/thoughtcrime/securesms/util/SaveAttachmentTask.java
+++ b/src/org/thoughtcrime/securesms/util/SaveAttachmentTask.java
@@ -2,15 +2,12 @@ package org.thoughtcrime.securesms.util;
import android.content.Context;
import android.content.DialogInterface.OnClickListener;
-import android.content.Intent;
import android.media.MediaScannerConnection;
import android.net.Uri;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
-import android.support.design.widget.Snackbar;
import android.support.v7.app.AlertDialog;
import android.util.Log;
-import android.view.View;
import android.webkit.MimeTypeMap;
import android.widget.Toast;
@@ -32,27 +29,25 @@ import java.text.SimpleDateFormat;
public class SaveAttachmentTask extends ProgressDialogAsyncTask> {
private static final String TAG = SaveAttachmentTask.class.getSimpleName();
- protected static final int SUCCESS = 0;
- protected static final int FAILURE = 1;
- protected static final int WRITE_ACCESS_FAILURE = 2;
+ static final int SUCCESS = 0;
+ private static final int FAILURE = 1;
+ private static final int WRITE_ACCESS_FAILURE = 2;
private final WeakReference contextReference;
private final WeakReference masterSecretReference;
- private final WeakReference view;
private final int attachmentCount;
- public SaveAttachmentTask(Context context, MasterSecret masterSecret, View view) {
- this(context, masterSecret, view, 1);
+ public SaveAttachmentTask(Context context, MasterSecret masterSecret) {
+ this(context, masterSecret, 1);
}
- public SaveAttachmentTask(Context context, MasterSecret masterSecret, View view, int count) {
+ public SaveAttachmentTask(Context context, MasterSecret masterSecret, int count) {
super(context,
context.getResources().getQuantityString(R.plurals.ConversationFragment_saving_n_attachments, count, count),
context.getResources().getQuantityString(R.plurals.ConversationFragment_saving_n_attachments_to_sd_card, count, count));
this.contextReference = new WeakReference<>(context);
this.masterSecretReference = new WeakReference<>(masterSecret);
- this.view = new WeakReference<>(view);
this.attachmentCount = count;
}
diff --git a/src/org/thoughtcrime/securesms/video/VideoPlayer.java b/src/org/thoughtcrime/securesms/video/VideoPlayer.java
index e34c16b4ef..78032be819 100644
--- a/src/org/thoughtcrime/securesms/video/VideoPlayer.java
+++ b/src/org/thoughtcrime/securesms/video/VideoPlayer.java
@@ -1,4 +1,4 @@
-/**
+/*
* Copyright (C) 2017 Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
@@ -95,11 +95,19 @@ public class VideoPlayer extends FrameLayout {
}
}
- public void setVideoSource(@NonNull MasterSecret masterSecret, @NonNull VideoSlide videoSource)
+ public void setVideoSource(@NonNull MasterSecret masterSecret, @NonNull VideoSlide videoSource, boolean autoplay)
throws IOException
{
- if (Build.VERSION.SDK_INT >= 16) setExoViewSource(masterSecret, videoSource);
- else setVideoViewSource(masterSecret, videoSource);
+ if (Build.VERSION.SDK_INT >= 16) setExoViewSource(masterSecret, videoSource, autoplay);
+ else setVideoViewSource(masterSecret, videoSource, autoplay);
+ }
+
+ public void pause() {
+ if (this.attachmentServer != null && this.videoView != null) {
+ this.videoView.stopPlayback();
+ } else if (this.exoPlayer != null) {
+ this.exoPlayer.setPlayWhenReady(false);
+ }
}
public void cleanup() {
@@ -112,11 +120,11 @@ public class VideoPlayer extends FrameLayout {
}
}
- public void setWindow(Window window) {
+ public void setWindow(@Nullable Window window) {
this.window = window;
}
- private void setExoViewSource(@NonNull MasterSecret masterSecret, @NonNull VideoSlide videoSource)
+ private void setExoViewSource(@NonNull MasterSecret masterSecret, @NonNull VideoSlide videoSource, boolean autoplay)
throws IOException
{
BandwidthMeter bandwidthMeter = new DefaultBandwidthMeter();
@@ -126,6 +134,7 @@ public class VideoPlayer extends FrameLayout {
exoPlayer = ExoPlayerFactory.newSimpleInstance(getContext(), trackSelector, loadControl);
exoPlayer.addListener(new ExoPlayerListener(window));
+ //noinspection ConstantConditions
exoView.setPlayer(exoPlayer);
DefaultDataSourceFactory defaultDataSourceFactory = new DefaultDataSourceFactory(getContext(), "GenericUserAgent", null);
@@ -135,10 +144,10 @@ public class VideoPlayer extends FrameLayout {
MediaSource mediaSource = new ExtractorMediaSource(videoSource.getUri(), attachmentDataSourceFactory, extractorsFactory, null, null);
exoPlayer.prepare(mediaSource);
- exoPlayer.setPlayWhenReady(true);
+ exoPlayer.setPlayWhenReady(autoplay);
}
- private void setVideoViewSource(@NonNull MasterSecret masterSecret, @NonNull VideoSlide videoSource)
+ private void setVideoViewSource(@NonNull MasterSecret masterSecret, @NonNull VideoSlide videoSource, boolean autoplay)
throws IOException
{
if (this.attachmentServer != null) {
@@ -150,16 +159,18 @@ public class VideoPlayer extends FrameLayout {
this.attachmentServer = new AttachmentServer(getContext(), masterSecret, videoSource.asAttachment());
this.attachmentServer.start();
+ //noinspection ConstantConditions
this.videoView.setVideoURI(this.attachmentServer.getUri());
} else if (videoSource.getUri() != null) {
Log.w(TAG, "Playing video directly from non-local Uri...");
+ //noinspection ConstantConditions
this.videoView.setVideoURI(videoSource.getUri());
} else {
Toast.makeText(getContext(), getContext().getString(R.string.VideoPlayer_error_playing_video), Toast.LENGTH_LONG).show();
return;
}
- this.videoView.start();
+ if (autoplay) this.videoView.start();
}
private void initializeVideoViewControls(@NonNull VideoView videoView) {