Add swipe gestures to media view

Fixes #2355
Closes #6632
This commit is contained in:
Moxie Marlinspike 2018-01-04 11:11:49 -08:00
parent 34424a9b3e
commit 8bba45f396
12 changed files with 578 additions and 112 deletions

View File

@ -1,20 +1,13 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="@color/gray95"> android:background="@color/gray95">
<org.thoughtcrime.securesms.components.ZoomingImageView <android.support.v4.view.ViewPager
android:id="@+id/image" android:id="@+id/media_pager"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:contentDescription="@string/media_preview_activity__media_content_description" />
<org.thoughtcrime.securesms.video.VideoPlayer
android:id="@+id/video_player"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"/>
android:visibility="gone"/>
</RelativeLayout> </FrameLayout>

20
res/layout/media_view.xml Normal file
View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<org.thoughtcrime.securesms.components.ZoomingImageView
android:id="@+id/image"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:contentDescription="@string/media_preview_activity__media_content_description" />
<org.thoughtcrime.securesms.video.VideoPlayer
android:id="@+id/video_player"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone"/>
</merge>

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<org.thoughtcrime.securesms.components.MediaView
android:id="@+id/media_view"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</FrameLayout>

View File

@ -391,7 +391,7 @@ public class ConversationFragment extends Fragment
public void onClick(DialogInterface dialog, int which) { public void onClick(DialogInterface dialog, int which) {
for (Slide slide : message.getSlideDeck().getSlides()) { for (Slide slide : message.getSlideDeck().getSlides()) {
if ((slide.hasImage() || slide.hasVideo() || slide.hasAudio() || slide.hasDocument()) && slide.getUri() != null) { 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())); saveTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, new Attachment(slide.getUri(), slide.getContentType(), message.getDateReceived(), slide.getFileName().orNull()));
return; return;
} }

View File

@ -17,27 +17,43 @@
package org.thoughtcrime.securesms; package org.thoughtcrime.securesms;
import android.Manifest; import android.Manifest;
import android.annotation.SuppressLint;
import android.annotation.TargetApi; import android.annotation.TargetApi;
import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.database.Cursor;
import android.net.Uri; import android.net.Uri;
import android.os.AsyncTask; import android.os.AsyncTask;
import android.os.Build.VERSION; import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES; import android.os.Build.VERSION_CODES;
import android.os.Bundle; import android.os.Bundle;
import android.support.annotation.NonNull; 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.util.Log;
import android.view.LayoutInflater;
import android.view.Menu; import android.view.Menu;
import android.view.MenuInflater; import android.view.MenuInflater;
import android.view.MenuItem; import android.view.MenuItem;
import android.view.View; import android.view.View;
import android.view.ViewGroup;
import android.view.Window;
import android.view.WindowManager; import android.view.WindowManager;
import android.widget.FrameLayout;
import android.widget.Toast; 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.crypto.MasterSecret;
import org.thoughtcrime.securesms.database.Address; 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.GlideApp;
import org.thoughtcrime.securesms.mms.VideoSlide; import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.permissions.Permissions; import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientModifiedListener; 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;
import org.thoughtcrime.securesms.util.SaveAttachmentTask.Attachment; import org.thoughtcrime.securesms.util.SaveAttachmentTask.Attachment;
import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.video.VideoPlayer;
import java.io.IOException; import java.io.IOException;
import java.util.WeakHashMap;
/** /**
* Activity for displaying media attachments in-app * Activity for displaying media attachments in-app
*/ */
public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity implements RecipientModifiedListener { public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity implements RecipientModifiedListener, LoaderManager.LoaderCallbacks<Pair<Cursor, Integer>> {
private final static String TAG = MediaPreviewActivity.class.getSimpleName(); private final static String TAG = MediaPreviewActivity.class.getSimpleName();
public static final String ADDRESS_EXTRA = "address"; public static final String ADDRESS_EXTRA = "address";
@ -65,16 +82,13 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
private MasterSecret masterSecret; private MasterSecret masterSecret;
private ZoomingImageView image; private ViewPager mediaPager;
private VideoPlayer video; private Uri initialMediaUri;
private String initialMediaType;
private Uri mediaUri; private long initialMediaSize;
private String mediaType; private Recipient conversationRecipient;
private Recipient recipient;
private long date;
private long size;
private boolean outgoing;
@SuppressWarnings("ConstantConditions")
@Override @Override
protected void onCreate(Bundle bundle, @NonNull MasterSecret masterSecret) { protected void onCreate(Bundle bundle, @NonNull MasterSecret masterSecret) {
this.masterSecret = masterSecret; this.masterSecret = masterSecret;
@ -90,7 +104,6 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
initializeViews(); initializeViews();
initializeResources(); initializeResources();
initializeActionBar();
} }
@Override @Override
@ -110,126 +123,126 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
Util.runOnMain(this::initializeActionBar); Util.runOnMain(this::initializeActionBar);
} }
@SuppressWarnings("ConstantConditions")
private void initializeActionBar() { private void initializeActionBar() {
final CharSequence relativeTimeSpan; MediaItem mediaItem = getCurrentMediaItem();
if (date > 0) { if (mediaItem != null) {
relativeTimeSpan = DateUtils.getExtendedRelativeTimeSpanString(this,dynamicLanguage.getCurrentLocale(),date); CharSequence relativeTimeSpan;
} else {
relativeTimeSpan = getString(R.string.MediaPreviewActivity_draft); 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);
} }
if (outgoing) getSupportActionBar().setTitle(getString(R.string.MediaPreviewActivity_you));
else getSupportActionBar().setTitle(recipient.toShortString());
getSupportActionBar().setSubtitle(relativeTimeSpan);
} }
@Override @Override
public void onResume() { public void onResume() {
super.onResume(); super.onResume();
dynamicLanguage.onResume(this); dynamicLanguage.onResume(this);
if (recipient != null) recipient.addListener(this);
initializeMedia(); initializeMedia();
} }
@Override @Override
public void onPause() { public void onPause() {
super.onPause(); super.onPause();
if (recipient != null) recipient.removeListener(this);
cleanupMedia(); cleanupMedia();
} }
@Override @Override
protected void onNewIntent(Intent intent) { protected void onNewIntent(Intent intent) {
super.onNewIntent(intent); super.onNewIntent(intent);
if (recipient != null) recipient.removeListener(this);
setIntent(intent); setIntent(intent);
initializeResources(); initializeResources();
initializeActionBar();
} }
private void initializeViews() { private void initializeViews() {
image = findViewById(R.id.image); mediaPager = findViewById(R.id.media_pager);
video = findViewById(R.id.video_player); mediaPager.setOffscreenPageLimit(1);
mediaPager.addOnPageChangeListener(new ViewPagerListener());
} }
private void initializeResources() { private void initializeResources() {
Address address = getIntent().getParcelableExtra(ADDRESS_EXTRA); Address address = getIntent().getParcelableExtra(ADDRESS_EXTRA);
mediaUri = getIntent().getData(); initialMediaUri = getIntent().getData();
mediaType = getIntent().getType(); initialMediaType = getIntent().getType();
date = getIntent().getLongExtra(DATE_EXTRA, -1); initialMediaSize = getIntent().getLongExtra(SIZE_EXTRA, 0);
size = getIntent().getLongExtra(SIZE_EXTRA, 0);
outgoing = getIntent().getBooleanExtra(OUTGOING_EXTRA, false);
if (address != null) { if (address != null) {
recipient = Recipient.from(this, address, true); conversationRecipient = Recipient.from(this, address, true);
recipient.addListener(this);
} else { } else {
recipient = null; conversationRecipient = null;
} }
} }
private void initializeMedia() { private void initializeMedia() {
if (!isContentTypeSupported(mediaType)) { if (!isContentTypeSupported(initialMediaType)) {
Log.w(TAG, "Unsupported media type sent to MediaPreviewActivity, finishing."); Log.w(TAG, "Unsupported media type sent to MediaPreviewActivity, finishing.");
Toast.makeText(getApplicationContext(), R.string.MediaPreviewActivity_unssuported_media_type, Toast.LENGTH_LONG).show(); Toast.makeText(getApplicationContext(), R.string.MediaPreviewActivity_unssuported_media_type, Toast.LENGTH_LONG).show();
finish(); finish();
} }
Log.w(TAG, "Loading Part URI: " + mediaUri); Log.w(TAG, "Loading Part URI: " + initialMediaUri);
try { if (conversationRecipient != null) {
if (mediaType != null && mediaType.startsWith("image/")) { getSupportLoaderManager().initLoader(0, null, this);
image.setVisibility(View.VISIBLE); } else {
video.setVisibility(View.GONE); mediaPager.setAdapter(new SingleItemPagerAdapter(this, masterSecret, GlideApp.with(this), getWindow(), initialMediaUri, initialMediaType, initialMediaSize));
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();
} }
} }
private void cleanupMedia() { private void cleanupMedia() {
image.cleanup(); mediaPager.removeAllViews();
video.cleanup(); mediaPager.setAdapter(null);
} }
private void showOverview() { private void showOverview() {
Intent intent = new Intent(this, MediaOverviewActivity.class); Intent intent = new Intent(this, MediaOverviewActivity.class);
intent.putExtra(MediaOverviewActivity.ADDRESS_EXTRA, recipient.getAddress()); intent.putExtra(MediaOverviewActivity.ADDRESS_EXTRA, conversationRecipient.getAddress());
startActivity(intent); startActivity(intent);
} }
private void forward() { private void forward() {
Intent composeIntent = new Intent(this, ShareActivity.class); MediaItem mediaItem = getCurrentMediaItem();
composeIntent.putExtra(Intent.EXTRA_STREAM, mediaUri);
composeIntent.setType(mediaType); if (mediaItem != null) {
startActivity(composeIntent); 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() { private void saveToDisk() {
SaveAttachmentTask.showWarningDialog(this, (dialogInterface, i) -> { MediaItem mediaItem = getCurrentMediaItem();
Permissions.with(this)
.request(Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE) if (mediaItem != null) {
.ifNecessary() SaveAttachmentTask.showWarningDialog(this, (dialogInterface, i) -> {
.withPermanentDenialDialog(getString(R.string.MediaPreviewActivity_signal_needs_the_storage_permission_in_order_to_write_to_external_storage_but_it_has_been_permanently_denied)) Permissions.with(this)
.onAnyDenied(() -> Toast.makeText(this, R.string.MediaPreviewActivity_unable_to_write_to_external_storage_without_permission, Toast.LENGTH_LONG).show()) .request(Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE)
.onAllGranted(() -> { .ifNecessary()
SaveAttachmentTask saveTask = new SaveAttachmentTask(MediaPreviewActivity.this, masterSecret, image); .withPermanentDenialDialog(getString(R.string.MediaPreviewActivity_signal_needs_the_storage_permission_in_order_to_write_to_external_storage_but_it_has_been_permanently_denied))
long saveDate = (date > 0) ? date : System.currentTimeMillis(); .onAnyDenied(() -> Toast.makeText(this, R.string.MediaPreviewActivity_unable_to_write_to_external_storage_without_permission, Toast.LENGTH_LONG).show())
saveTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, new Attachment(mediaUri, mediaType, saveDate, null)); .onAllGranted(() -> {
}) SaveAttachmentTask saveTask = new SaveAttachmentTask(MediaPreviewActivity.this, masterSecret);
.execute(); 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 @Override
@ -239,7 +252,7 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
menu.clear(); menu.clear();
MenuInflater inflater = this.getMenuInflater(); MenuInflater inflater = this.getMenuInflater();
inflater.inflate(R.menu.media_preview, menu); 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; return true;
} }
@ -258,7 +271,260 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
return false; 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) { public static boolean isContentTypeSupported(final String contentType) {
return contentType != null && (contentType.startsWith("image/") || contentType.startsWith("video/")); return contentType != null && (contentType.startsWith("image/") || contentType.startsWith("video/"));
} }
@Override
public Loader<Pair<Cursor, Integer>> onCreateLoader(int id, Bundle args) {
return new PagingMediaLoader(this, conversationRecipient, initialMediaUri);
}
@Override
public void onLoadFinished(Loader<Pair<Cursor, Integer>> loader, @Nullable Pair<Cursor, Integer> 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<Pair<Cursor, Integer>> 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<Integer, MediaView> 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);
}
} }

View File

@ -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();
}
}

View File

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

View File

@ -67,7 +67,7 @@ public class AttachmentDatabase extends Database {
static final String TABLE_NAME = "part"; static final String TABLE_NAME = "part";
static final String ROW_ID = "_id"; 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 MMS_ID = "mid";
static final String CONTENT_TYPE = "ct"; static final String CONTENT_TYPE = "ct";
static final String NAME = "name"; static final String NAME = "name";
@ -79,7 +79,7 @@ public class AttachmentDatabase extends Database {
static final String FILE_NAME = "file_name"; static final String FILE_NAME = "file_name";
static final String THUMBNAIL = "thumbnail"; static final String THUMBNAIL = "thumbnail";
static final String THUMBNAIL_ASPECT_RATIO = "aspect_ratio"; 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 DIGEST = "digest";
static final String VOICE_NOTE = "voice_note"; static final String VOICE_NOTE = "voice_note";
public static final String FAST_PREFLIGHT_ID = "fast_preflight_id"; public static final String FAST_PREFLIGHT_ID = "fast_preflight_id";

View File

@ -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<Pair<Cursor, Integer>> {
@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<Cursor, Integer> 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;
}
}

View File

@ -1,5 +1,6 @@
package org.thoughtcrime.securesms.util; package org.thoughtcrime.securesms.util;
import android.annotation.SuppressLint;
import android.content.Context; import android.content.Context;
import android.database.Cursor; import android.database.Cursor;
import android.support.v4.content.AsyncTaskLoader; 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. * to get the benefits of reloading when content has changed.
*/ */
public abstract class AbstractCursorLoader extends AsyncTaskLoader<Cursor> { public abstract class AbstractCursorLoader extends AsyncTaskLoader<Cursor> {
@SuppressWarnings("unused")
private static final String TAG = AbstractCursorLoader.class.getSimpleName(); private static final String TAG = AbstractCursorLoader.class.getSimpleName();
protected final ForceLoadContentObserver observer; @SuppressLint("StaticFieldLeak")
protected final Context context; protected final Context context;
private final ForceLoadContentObserver observer;
protected Cursor cursor; protected Cursor cursor;
public AbstractCursorLoader(Context context) { public AbstractCursorLoader(Context context) {

View File

@ -2,15 +2,12 @@ package org.thoughtcrime.securesms.util;
import android.content.Context; import android.content.Context;
import android.content.DialogInterface.OnClickListener; import android.content.DialogInterface.OnClickListener;
import android.content.Intent;
import android.media.MediaScannerConnection; import android.media.MediaScannerConnection;
import android.net.Uri; import android.net.Uri;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.support.design.widget.Snackbar;
import android.support.v7.app.AlertDialog; import android.support.v7.app.AlertDialog;
import android.util.Log; import android.util.Log;
import android.view.View;
import android.webkit.MimeTypeMap; import android.webkit.MimeTypeMap;
import android.widget.Toast; import android.widget.Toast;
@ -32,27 +29,25 @@ import java.text.SimpleDateFormat;
public class SaveAttachmentTask extends ProgressDialogAsyncTask<SaveAttachmentTask.Attachment, Void, Pair<Integer, String>> { public class SaveAttachmentTask extends ProgressDialogAsyncTask<SaveAttachmentTask.Attachment, Void, Pair<Integer, String>> {
private static final String TAG = SaveAttachmentTask.class.getSimpleName(); private static final String TAG = SaveAttachmentTask.class.getSimpleName();
protected static final int SUCCESS = 0; static final int SUCCESS = 0;
protected static final int FAILURE = 1; private static final int FAILURE = 1;
protected static final int WRITE_ACCESS_FAILURE = 2; private static final int WRITE_ACCESS_FAILURE = 2;
private final WeakReference<Context> contextReference; private final WeakReference<Context> contextReference;
private final WeakReference<MasterSecret> masterSecretReference; private final WeakReference<MasterSecret> masterSecretReference;
private final WeakReference<View> view;
private final int attachmentCount; private final int attachmentCount;
public SaveAttachmentTask(Context context, MasterSecret masterSecret, View view) { public SaveAttachmentTask(Context context, MasterSecret masterSecret) {
this(context, masterSecret, view, 1); this(context, masterSecret, 1);
} }
public SaveAttachmentTask(Context context, MasterSecret masterSecret, View view, int count) { public SaveAttachmentTask(Context context, MasterSecret masterSecret, int count) {
super(context, super(context,
context.getResources().getQuantityString(R.plurals.ConversationFragment_saving_n_attachments, count, count), 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)); context.getResources().getQuantityString(R.plurals.ConversationFragment_saving_n_attachments_to_sd_card, count, count));
this.contextReference = new WeakReference<>(context); this.contextReference = new WeakReference<>(context);
this.masterSecretReference = new WeakReference<>(masterSecret); this.masterSecretReference = new WeakReference<>(masterSecret);
this.view = new WeakReference<>(view);
this.attachmentCount = count; this.attachmentCount = count;
} }

View File

@ -1,4 +1,4 @@
/** /*
* Copyright (C) 2017 Whisper Systems * Copyright (C) 2017 Whisper Systems
* *
* This program is free software: you can redistribute it and/or modify * 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 throws IOException
{ {
if (Build.VERSION.SDK_INT >= 16) setExoViewSource(masterSecret, videoSource); if (Build.VERSION.SDK_INT >= 16) setExoViewSource(masterSecret, videoSource, autoplay);
else setVideoViewSource(masterSecret, videoSource); 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() { 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; 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 throws IOException
{ {
BandwidthMeter bandwidthMeter = new DefaultBandwidthMeter(); BandwidthMeter bandwidthMeter = new DefaultBandwidthMeter();
@ -126,6 +134,7 @@ public class VideoPlayer extends FrameLayout {
exoPlayer = ExoPlayerFactory.newSimpleInstance(getContext(), trackSelector, loadControl); exoPlayer = ExoPlayerFactory.newSimpleInstance(getContext(), trackSelector, loadControl);
exoPlayer.addListener(new ExoPlayerListener(window)); exoPlayer.addListener(new ExoPlayerListener(window));
//noinspection ConstantConditions
exoView.setPlayer(exoPlayer); exoView.setPlayer(exoPlayer);
DefaultDataSourceFactory defaultDataSourceFactory = new DefaultDataSourceFactory(getContext(), "GenericUserAgent", null); 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); MediaSource mediaSource = new ExtractorMediaSource(videoSource.getUri(), attachmentDataSourceFactory, extractorsFactory, null, null);
exoPlayer.prepare(mediaSource); 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 throws IOException
{ {
if (this.attachmentServer != null) { if (this.attachmentServer != null) {
@ -150,16 +159,18 @@ public class VideoPlayer extends FrameLayout {
this.attachmentServer = new AttachmentServer(getContext(), masterSecret, videoSource.asAttachment()); this.attachmentServer = new AttachmentServer(getContext(), masterSecret, videoSource.asAttachment());
this.attachmentServer.start(); this.attachmentServer.start();
//noinspection ConstantConditions
this.videoView.setVideoURI(this.attachmentServer.getUri()); this.videoView.setVideoURI(this.attachmentServer.getUri());
} else if (videoSource.getUri() != null) { } else if (videoSource.getUri() != null) {
Log.w(TAG, "Playing video directly from non-local Uri..."); Log.w(TAG, "Playing video directly from non-local Uri...");
//noinspection ConstantConditions
this.videoView.setVideoURI(videoSource.getUri()); this.videoView.setVideoURI(videoSource.getUri());
} else { } else {
Toast.makeText(getContext(), getContext().getString(R.string.VideoPlayer_error_playing_video), Toast.LENGTH_LONG).show(); Toast.makeText(getContext(), getContext().getString(R.string.VideoPlayer_error_playing_video), Toast.LENGTH_LONG).show();
return; return;
} }
this.videoView.start(); if (autoplay) this.videoView.start();
} }
private void initializeVideoViewControls(@NonNull VideoView videoView) { private void initializeVideoViewControls(@NonNull VideoView videoView) {