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) {