diff --git a/res/drawable-hdpi/ic_caption.png b/res/drawable-hdpi/ic_caption.png
new file mode 100644
index 0000000000..d63dfa1d27
Binary files /dev/null and b/res/drawable-hdpi/ic_caption.png differ
diff --git a/res/drawable-mdpi/ic_caption.png b/res/drawable-mdpi/ic_caption.png
new file mode 100644
index 0000000000..bc2a88f747
Binary files /dev/null and b/res/drawable-mdpi/ic_caption.png differ
diff --git a/res/drawable-xhdpi/ic_caption.png b/res/drawable-xhdpi/ic_caption.png
new file mode 100644
index 0000000000..48d0d69c55
Binary files /dev/null and b/res/drawable-xhdpi/ic_caption.png differ
diff --git a/res/drawable-xxhdpi/ic_caption.png b/res/drawable-xxhdpi/ic_caption.png
new file mode 100644
index 0000000000..6892ef867c
Binary files /dev/null and b/res/drawable-xxhdpi/ic_caption.png differ
diff --git a/res/drawable-xxxhdpi/ic_caption.png b/res/drawable-xxxhdpi/ic_caption.png
new file mode 100644
index 0000000000..5ce643d2d1
Binary files /dev/null and b/res/drawable-xxxhdpi/ic_caption.png differ
diff --git a/res/drawable/album_rail_item_background.xml b/res/drawable/album_rail_item_background.xml
new file mode 100644
index 0000000000..6a2d6259fb
--- /dev/null
+++ b/res/drawable/album_rail_item_background.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/res/layout-v16/video_player.xml b/res/layout-v16/video_player.xml
index ba3075f06f..b855fca65a 100644
--- a/res/layout-v16/video_player.xml
+++ b/res/layout-v16/video_player.xml
@@ -1,14 +1,17 @@
-
+
+ android:id="@+id/video_view"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_gravity="center"
+ android:gravity="center"
+ app:player_layout_id="@layout/media_preview_exoplayer_layout"/>
\ No newline at end of file
diff --git a/res/layout/album_thumbnail_2.xml b/res/layout/album_thumbnail_2.xml
new file mode 100644
index 0000000000..8750abd1eb
--- /dev/null
+++ b/res/layout/album_thumbnail_2.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/res/layout/album_thumbnail_3.xml b/res/layout/album_thumbnail_3.xml
new file mode 100644
index 0000000000..64b7b55ea8
--- /dev/null
+++ b/res/layout/album_thumbnail_3.xml
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/res/layout/album_thumbnail_4.xml b/res/layout/album_thumbnail_4.xml
new file mode 100644
index 0000000000..205148365e
--- /dev/null
+++ b/res/layout/album_thumbnail_4.xml
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/res/layout/album_thumbnail_5.xml b/res/layout/album_thumbnail_5.xml
new file mode 100644
index 0000000000..2347e9b33b
--- /dev/null
+++ b/res/layout/album_thumbnail_5.xml
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/res/layout/album_thumbnail_many.xml b/res/layout/album_thumbnail_many.xml
new file mode 100644
index 0000000000..004ee8323c
--- /dev/null
+++ b/res/layout/album_thumbnail_many.xml
@@ -0,0 +1,61 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/res/layout/album_thumbnail_view.xml b/res/layout/album_thumbnail_view.xml
new file mode 100644
index 0000000000..3bac5cb0e1
--- /dev/null
+++ b/res/layout/album_thumbnail_view.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/res/layout/conversation_item_thumbnail.xml b/res/layout/conversation_item_thumbnail.xml
index 3876fa7e08..55bbbefa16 100644
--- a/res/layout/conversation_item_thumbnail.xml
+++ b/res/layout/conversation_item_thumbnail.xml
@@ -1,7 +1,8 @@
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools">
+
+
-
+
+
+
+
+
+ android:layout_height="wrap_content"
+ android:paddingTop="32dp"
+ android:animateLayoutChanges="true"
+ app:scrollView_maxHeight="120dp">
+
+
+
+
+
+
+
+
+
+
diff --git a/res/layout/media_preview_album_rail_item.xml b/res/layout/media_preview_album_rail_item.xml
new file mode 100644
index 0000000000..df2ca67c16
--- /dev/null
+++ b/res/layout/media_preview_album_rail_item.xml
@@ -0,0 +1,12 @@
+
+
+
diff --git a/res/layout/media_preview_exoplayer_layout.xml b/res/layout/media_preview_exoplayer_layout.xml
new file mode 100644
index 0000000000..d6b870c7c5
--- /dev/null
+++ b/res/layout/media_preview_exoplayer_layout.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/res/layout/media_view.xml b/res/layout/media_view.xml
index dd6a39ad7e..60f83751d4 100644
--- a/res/layout/media_view.xml
+++ b/res/layout/media_view.xml
@@ -1,5 +1,6 @@
@@ -8,6 +9,7 @@
android:id="@+id/image"
android:layout_width="match_parent"
android:layout_height="match_parent"
+ android:clickable="false"
android:contentDescription="@string/media_preview_activity__media_content_description" />
+
+
-
+
+
+
+
+
+
+
+
diff --git a/res/values/attrs.xml b/res/values/attrs.xml
index 3b4d3ea751..290234bde1 100644
--- a/res/values/attrs.xml
+++ b/res/values/attrs.xml
@@ -86,6 +86,7 @@
+
@@ -300,4 +301,8 @@
+
+
+
+
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 087e22fb32..036e232750 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -10,6 +10,9 @@
New message
+
+ \+%d
+
Currently: %s
You haven\'t set a passphrase yet!
@@ -734,6 +737,12 @@
Signal
New message
+
+
+ - %d Item
+ - %d Items
+
+
Device no longer registered
This is likely because you registered your phone number with Signal on a different device. Tap to re-register.
diff --git a/res/values/themes.xml b/res/values/themes.xml
index b17c51bd5d..7aa071c51c 100644
--- a/res/values/themes.xml
+++ b/res/values/themes.xml
@@ -204,6 +204,7 @@
- @color/core_grey_90
- @drawable/sticky_date_header_background_light
- @color/core_grey_60
+ - @color/transparent_black_30
- @drawable/quick_camera_light
- @drawable/ic_mic_grey600_24dp
@@ -311,6 +312,7 @@
- @color/core_grey_05
- @drawable/sticky_date_header_background_dark
- @color/core_grey_25
+ - @color/transparent_white_30
- @drawable/contact_list_divider_dark
diff --git a/src/org/thoughtcrime/securesms/ConversationItem.java b/src/org/thoughtcrime/securesms/ConversationItem.java
index 2f9fa25d3f..0aa42ae246 100644
--- a/src/org/thoughtcrime/securesms/ConversationItem.java
+++ b/src/org/thoughtcrime/securesms/ConversationItem.java
@@ -42,7 +42,6 @@ import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;
-import org.thoughtcrime.securesms.attachments.Attachment;
import org.thoughtcrime.securesms.attachments.DatabaseAttachment;
import org.thoughtcrime.securesms.components.AlertView;
import org.thoughtcrime.securesms.components.AudioView;
@@ -53,7 +52,6 @@ import org.thoughtcrime.securesms.components.DocumentView;
import org.thoughtcrime.securesms.components.QuoteView;
import org.thoughtcrime.securesms.components.SharedContactView;
import org.thoughtcrime.securesms.contactshare.Contact;
-import org.thoughtcrime.securesms.database.AttachmentDatabase;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.MmsDatabase;
import org.thoughtcrime.securesms.database.MmsSmsDatabase;
@@ -71,6 +69,7 @@ import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.mms.PartAuthority;
import org.thoughtcrime.securesms.mms.Slide;
import org.thoughtcrime.securesms.mms.SlideClickListener;
+import org.thoughtcrime.securesms.mms.SlidesClickedListener;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientModifiedListener;
import org.thoughtcrime.securesms.util.DateUtils;
@@ -83,6 +82,7 @@ import org.thoughtcrime.securesms.util.ViewUtil;
import org.thoughtcrime.securesms.util.views.Stub;
import org.whispersystems.libsignal.util.guava.Optional;
+import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
@@ -132,10 +132,11 @@ public class ConversationItem extends LinearLayout
private int defaultBubbleColor;
private int measureCalls;
- private final PassthroughClickListener passthroughClickListener = new PassthroughClickListener();
- private final AttachmentDownloadClickListener downloadClickListener = new AttachmentDownloadClickListener();
- private final SharedContactEventListener sharedContactEventListener = new SharedContactEventListener();
- private final SharedContactClickListener sharedContactClickListener = new SharedContactClickListener();
+ private final PassthroughClickListener passthroughClickListener = new PassthroughClickListener();
+ private final AttachmentDownloadClickListener downloadClickListener = new AttachmentDownloadClickListener();
+ private final SlideClickPassthroughListener singleDownloadClickListener = new SlideClickPassthroughListener(downloadClickListener);
+ private final SharedContactEventListener sharedContactEventListener = new SharedContactEventListener();
+ private final SharedContactClickListener sharedContactClickListener = new SharedContactClickListener();
private final Context context;
@@ -427,7 +428,7 @@ public class ConversationItem extends LinearLayout
//noinspection ConstantConditions
audioViewStub.get().setAudio(((MediaMmsMessageRecord) messageRecord).getSlideDeck().getAudioSlide(), showControls);
- audioViewStub.get().setDownloadClickListener(downloadClickListener);
+ audioViewStub.get().setDownloadClickListener(singleDownloadClickListener);
audioViewStub.get().setOnLongClickListener(passthroughClickListener);
ViewUtil.updateLayoutParams(bodyText, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
@@ -442,7 +443,7 @@ public class ConversationItem extends LinearLayout
//noinspection ConstantConditions
documentViewStub.get().setDocument(((MediaMmsMessageRecord)messageRecord).getSlideDeck().getDocumentSlide(), showControls);
documentViewStub.get().setDocumentClickListener(new ThumbnailClickListener());
- documentViewStub.get().setDownloadClickListener(downloadClickListener);
+ documentViewStub.get().setDownloadClickListener(singleDownloadClickListener);
documentViewStub.get().setOnLongClickListener(passthroughClickListener);
ViewUtil.updateLayoutParams(bodyText, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
@@ -455,19 +456,18 @@ public class ConversationItem extends LinearLayout
if (sharedContactStub.resolved()) sharedContactStub.get().setVisibility(GONE);
//noinspection ConstantConditions
- Slide thumbnailSlide = ((MmsMessageRecord) messageRecord).getSlideDeck().getThumbnailSlide();
- Attachment attachment = thumbnailSlide.asAttachment();
+ List thumbnailSlides = ((MmsMessageRecord) messageRecord).getSlideDeck().getThumbnailSlides();
mediaThumbnailStub.get().setImageResource(glideRequests,
- thumbnailSlide,
+ thumbnailSlides,
showControls,
- false,
- attachment.getWidth(),
- attachment.getHeight());
+ false);
mediaThumbnailStub.get().setThumbnailClickListener(new ThumbnailClickListener());
mediaThumbnailStub.get().setDownloadClickListener(downloadClickListener);
mediaThumbnailStub.get().setOnLongClickListener(passthroughClickListener);
mediaThumbnailStub.get().setOnClickListener(passthroughClickListener);
mediaThumbnailStub.get().showShade(TextUtils.isEmpty(messageRecord.getDisplayBody()));
+ mediaThumbnailStub.get().setConversationColor(messageRecord.isOutgoing() ? defaultBubbleColor
+ : messageRecord.getRecipient().getColor().toConversationColor(context));
setThumbnailOutlineCorners(messageRecord, previousRecord, nextRecord, isGroupThread);
@@ -847,9 +847,9 @@ public class ConversationItem extends LinearLayout
}
}
- private class AttachmentDownloadClickListener implements SlideClickListener {
+ private class AttachmentDownloadClickListener implements SlidesClickedListener {
@Override
- public void onClick(View v, final Slide slide) {
+ public void onClick(View v, final List slides) {
Log.i(TAG, "onClick() for attachment download");
if (messageRecord.isMmsNotification()) {
Log.i(TAG, "Scheduling MMS attachment download");
@@ -858,19 +858,32 @@ public class ConversationItem extends LinearLayout
.add(new MmsDownloadJob(context, messageRecord.getId(),
messageRecord.getThreadId(), false));
} else {
- Log.i(TAG, "Scheduling push attachment download");
- DatabaseFactory.getAttachmentDatabase(context).setTransferState(messageRecord.getId(),
- slide.asAttachment(),
- AttachmentDatabase.TRANSFER_PROGRESS_STARTED);
+ Log.i(TAG, "Scheduling push attachment downloads for " + slides.size() + " items");
- ApplicationContext.getInstance(context)
- .getJobManager()
- .add(new AttachmentDownloadJob(context, messageRecord.getId(),
- ((DatabaseAttachment)slide.asAttachment()).getAttachmentId(), true));
+ for (Slide slide : slides) {
+ ApplicationContext.getInstance(context)
+ .getJobManager()
+ .add(new AttachmentDownloadJob(context, messageRecord.getId(),
+ ((DatabaseAttachment)slide.asAttachment()).getAttachmentId(), true));
+ }
}
}
}
+ private class SlideClickPassthroughListener implements SlideClickListener {
+
+ private final SlidesClickedListener original;
+
+ private SlideClickPassthroughListener(@NonNull SlidesClickedListener original) {
+ this.original = original;
+ }
+
+ @Override
+ public void onClick(View v, Slide slide) {
+ original.onClick(v, Collections.singletonList(slide));
+ }
+ }
+
private class ThumbnailClickListener implements SlideClickListener {
public void onClick(final View v, final Slide slide) {
if (shouldInterceptClicks(messageRecord) || !batchSelected.isEmpty()) {
@@ -883,6 +896,7 @@ public class ConversationItem extends LinearLayout
intent.putExtra(MediaPreviewActivity.OUTGOING_EXTRA, messageRecord.isOutgoing());
intent.putExtra(MediaPreviewActivity.DATE_EXTRA, messageRecord.getTimestamp());
intent.putExtra(MediaPreviewActivity.SIZE_EXTRA, slide.asAttachment().getSize());
+ intent.putExtra(MediaPreviewActivity.CAPTION_EXTRA, slide.getCaption().orNull());
intent.putExtra(MediaPreviewActivity.LEFT_IS_RECENT_EXTRA, false);
context.startActivity(intent);
diff --git a/src/org/thoughtcrime/securesms/MediaPreviewActivity.java b/src/org/thoughtcrime/securesms/MediaPreviewActivity.java
index 612f525eda..9af32fe3cf 100644
--- a/src/org/thoughtcrime/securesms/MediaPreviewActivity.java
+++ b/src/org/thoughtcrime/securesms/MediaPreviewActivity.java
@@ -19,6 +19,7 @@ package org.thoughtcrime.securesms;
import android.Manifest;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
+import android.arch.lifecycle.ViewModelProviders;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
@@ -36,15 +37,21 @@ import android.support.v4.view.PagerAdapter;
import android.support.v4.view.ViewPager;
import android.support.v7.app.AlertDialog;
import org.thoughtcrime.securesms.logging.Log;
+
+import android.support.v7.widget.LinearLayoutManager;
+import android.support.v7.widget.RecyclerView;
+import android.view.GestureDetector;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
+import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.Window;
import android.view.WindowManager;
import android.widget.FrameLayout;
+import android.widget.TextView;
import android.widget.Toast;
import org.thoughtcrime.securesms.attachments.DatabaseAttachment;
@@ -53,6 +60,8 @@ import org.thoughtcrime.securesms.components.viewpager.ExtendedOnPageChangedList
import org.thoughtcrime.securesms.database.Address;
import org.thoughtcrime.securesms.database.MediaDatabase.MediaRecord;
import org.thoughtcrime.securesms.database.loaders.PagingMediaLoader;
+import org.thoughtcrime.securesms.mediapreview.AlbumRailAdapter;
+import org.thoughtcrime.securesms.mediapreview.MediaPreviewViewModel;
import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.permissions.Permissions;
@@ -71,33 +80,49 @@ import java.util.WeakHashMap;
/**
* Activity for displaying media attachments in-app
*/
-public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity implements RecipientModifiedListener, LoaderManager.LoaderCallbacks> {
+public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity implements RecipientModifiedListener,
+ LoaderManager.LoaderCallbacks>,
+ AlbumRailAdapter.RailItemClickedListener
+{
private final static String TAG = MediaPreviewActivity.class.getSimpleName();
public static final String ADDRESS_EXTRA = "address";
public static final String DATE_EXTRA = "date";
public static final String SIZE_EXTRA = "size";
+ public static final String CAPTION_EXTRA = "caption";
public static final String OUTGOING_EXTRA = "outgoing";
public static final String LEFT_IS_RECENT_EXTRA = "left_is_recent";
private final DynamicLanguage dynamicLanguage = new DynamicLanguage();
- private ViewPager mediaPager;
- private Uri initialMediaUri;
- private String initialMediaType;
- private long initialMediaSize;
- private Recipient conversationRecipient;
- private boolean leftIsRecent;
+ private ViewPager mediaPager;
+ private View detailsContainer;
+ private TextView caption;
+ private View captionContainer;
+ private RecyclerView albumRail;
+ private AlbumRailAdapter albumRailAdapter;
+ private ViewGroup playbackControlsContainer;
+ private Uri initialMediaUri;
+ private String initialMediaType;
+ private long initialMediaSize;
+ private String initialCaption;
+ private Recipient conversationRecipient;
+ private boolean leftIsRecent;
+ private GestureDetector clickDetector;
+ private MediaPreviewViewModel viewModel;
private int restartItem = -1;
+
@SuppressWarnings("ConstantConditions")
@Override
protected void onCreate(Bundle bundle, boolean ready) {
this.setTheme(R.style.TextSecure_DarkTheme);
dynamicLanguage.onCreate(this);
+ viewModel = ViewModelProviders.of(this).get(MediaPreviewViewModel.class);
+
setFullscreenIfPossible();
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN);
@@ -107,6 +132,13 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
initializeViews();
initializeResources();
+ initializeObservers();
+ }
+
+ @Override
+ public boolean dispatchTouchEvent(MotionEvent ev) {
+ clickDetector.onTouchEvent(ev);
+ return super.dispatchTouchEvent(ev);
}
@Override
@@ -126,6 +158,11 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
Util.runOnMain(this::initializeActionBar);
}
+ @Override
+ public void onRailItemClicked(int distanceFromActive) {
+ mediaPager.setCurrentItem(mediaPager.getCurrentItem() + distanceFromActive);
+ }
+
@SuppressWarnings("ConstantConditions")
private void initializeActionBar() {
MediaItem mediaItem = getCurrentMediaItem();
@@ -172,6 +209,17 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
mediaPager = findViewById(R.id.media_pager);
mediaPager.setOffscreenPageLimit(1);
mediaPager.addOnPageChangeListener(new ViewPagerListener());
+
+ albumRail = findViewById(R.id.media_preview_album_rail);
+ albumRailAdapter = new AlbumRailAdapter(GlideApp.with(this), this);
+
+ albumRail.setLayoutManager(new LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false));
+ albumRail.setAdapter(albumRailAdapter);
+
+ detailsContainer = findViewById(R.id.media_preview_details_container);
+ caption = findViewById(R.id.media_preview_caption);
+ captionContainer = findViewById(R.id.media_preview_caption_container);
+ playbackControlsContainer = findViewById(R.id.media_preview_playback_controls_container);
}
private void initializeResources() {
@@ -180,6 +228,7 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
initialMediaUri = getIntent().getData();
initialMediaType = getIntent().getType();
initialMediaSize = getIntent().getLongExtra(SIZE_EXTRA, 0);
+ initialCaption = getIntent().getStringExtra(CAPTION_EXTRA);
leftIsRecent = getIntent().getBooleanExtra(LEFT_IS_RECENT_EXTRA, false);
restartItem = -1;
@@ -190,6 +239,49 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
}
}
+ private void initializeObservers() {
+ viewModel.getPreviewData().observe(this, previewData -> {
+ if (previewData == null) {
+ return;
+ }
+
+ View playbackControls = ((MediaItemAdapter) mediaPager.getAdapter()).getPlaybackControls(mediaPager.getCurrentItem());
+
+ if (previewData.getAlbumThumbnails().isEmpty() && previewData.getCaption() == null && playbackControls == null) {
+ detailsContainer.setVisibility(View.GONE);
+ } else {
+ detailsContainer.setVisibility(View.VISIBLE);
+ }
+
+ albumRail.setVisibility(previewData.getAlbumThumbnails().isEmpty() ? View.GONE : View.VISIBLE);
+ albumRailAdapter.setRecords(previewData.getAlbumThumbnails(), previewData.getActivePosition());
+ albumRail.smoothScrollToPosition(previewData.getActivePosition());
+
+ captionContainer.setVisibility(previewData.getCaption() == null ? View.GONE : View.VISIBLE);
+ caption.setText(previewData.getCaption());
+
+ if (playbackControls != null) {
+ ViewGroup.LayoutParams params = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
+ playbackControls.setLayoutParams(params);
+
+ playbackControlsContainer.removeAllViews();
+ playbackControlsContainer.addView(playbackControls);
+ } else {
+ playbackControlsContainer.removeAllViews();
+ }
+ });
+
+ clickDetector = new GestureDetector(new GestureDetector.SimpleOnGestureListener() {
+ @Override
+ public boolean onSingleTapUp(MotionEvent e) {
+ if (e.getY() < detailsContainer.getTop()) {
+ detailsContainer.setVisibility(detailsContainer.getVisibility() == View.VISIBLE ? View.GONE : View.VISIBLE);
+ }
+ return super.onSingleTapUp(e);
+ }
+ });
+ }
+
private void initializeMedia() {
if (!isContentTypeSupported(initialMediaType)) {
Log.w(TAG, "Unsupported media type sent to MediaPreviewActivity, finishing.");
@@ -203,6 +295,12 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
getSupportLoaderManager().restartLoader(0, null, this);
} else {
mediaPager.setAdapter(new SingleItemPagerAdapter(this, GlideApp.with(this), getWindow(), initialMediaUri, initialMediaType, initialMediaSize));
+
+ if (initialCaption != null) {
+ detailsContainer.setVisibility(View.VISIBLE);
+ captionContainer.setVisibility(View.VISIBLE);
+ caption.setText(initialCaption);
+ }
}
}
@@ -348,6 +446,8 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
mediaPager.setAdapter(adapter);
adapter.setActive(true);
+ viewModel.setCursor(data.first, leftIsRecent);
+
if (restartItem < 0) mediaPager.setCurrentItem(data.second);
else mediaPager.setCurrentItem(restartItem);
}
@@ -369,7 +469,7 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
if (adapter != null) {
MediaItem item = adapter.getMediaItemFor(position);
if (item.recipient != null) item.recipient.addListener(MediaPreviewActivity.this);
-
+ viewModel.setActiveAlbumRailItem(MediaPreviewActivity.this, position);
initializeActionBar();
}
}
@@ -453,6 +553,11 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
public void pause(int position) {
}
+
+ @Override
+ public @Nullable View getPlaybackControls(int position) {
+ return null;
+ }
}
private static class CursorPagerAdapter extends PagerAdapter implements MediaItemAdapter {
@@ -511,7 +616,8 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
try {
//noinspection ConstantConditions
- mediaView.set(glideRequests, window, mediaRecord.getAttachment().getDataUri(), mediaRecord.getAttachment().getContentType(), mediaRecord.getAttachment().getSize(), autoplay);
+ mediaView.set(glideRequests, window, mediaRecord.getAttachment().getDataUri(),
+ mediaRecord.getAttachment().getContentType(), mediaRecord.getAttachment().getSize(), autoplay);
} catch (IOException e) {
Log.w(TAG, e);
}
@@ -552,6 +658,13 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
if (mediaView != null) mediaView.pause();
}
+ @Override
+ public @Nullable View getPlaybackControls(int position) {
+ MediaView mediaView = mediaViews.get(position);
+ if (mediaView != null) return mediaView.getPlaybackControls();
+ return null;
+ }
+
private int getCursorPosition(int position) {
if (leftIsRecent) return position;
else return cursor.getCount() - 1 - position;
@@ -585,5 +698,6 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
interface MediaItemAdapter {
MediaItem getMediaItemFor(int position);
void pause(int position);
+ @Nullable View getPlaybackControls(int position);
}
}
diff --git a/src/org/thoughtcrime/securesms/RecipientPreferenceActivity.java b/src/org/thoughtcrime/securesms/RecipientPreferenceActivity.java
index df5010dee9..40c2f2ba48 100644
--- a/src/org/thoughtcrime/securesms/RecipientPreferenceActivity.java
+++ b/src/org/thoughtcrime/securesms/RecipientPreferenceActivity.java
@@ -178,6 +178,7 @@ public class RecipientPreferenceActivity extends PassphraseRequiredActionBarActi
intent.putExtra(MediaPreviewActivity.OUTGOING_EXTRA, mediaRecord.isOutgoing());
intent.putExtra(MediaPreviewActivity.DATE_EXTRA, mediaRecord.getDate());
intent.putExtra(MediaPreviewActivity.SIZE_EXTRA, mediaRecord.getAttachment().getSize());
+ intent.putExtra(MediaPreviewActivity.CAPTION_EXTRA, mediaRecord.getAttachment().getCaption());
intent.putExtra(MediaPreviewActivity.LEFT_IS_RECENT_EXTRA, ViewCompat.getLayoutDirection(threadPhotoRailView) == ViewCompat.LAYOUT_DIRECTION_LTR);
intent.setDataAndType(mediaRecord.getAttachment().getDataUri(), mediaRecord.getContentType());
startActivity(intent);
diff --git a/src/org/thoughtcrime/securesms/attachments/Attachment.java b/src/org/thoughtcrime/securesms/attachments/Attachment.java
index 7cc795d853..c639811ad9 100644
--- a/src/org/thoughtcrime/securesms/attachments/Attachment.java
+++ b/src/org/thoughtcrime/securesms/attachments/Attachment.java
@@ -37,10 +37,13 @@ public abstract class Attachment {
private final boolean quote;
+ @Nullable
+ private final String caption;
+
public Attachment(@NonNull String contentType, int transferState, long size, @Nullable String fileName,
@Nullable String location, @Nullable String key, @Nullable String relay,
@Nullable byte[] digest, @Nullable String fastPreflightId, boolean voiceNote,
- int width, int height, boolean quote)
+ int width, int height, boolean quote, @Nullable String caption)
{
this.contentType = contentType;
this.transferState = transferState;
@@ -55,6 +58,7 @@ public abstract class Attachment {
this.width = width;
this.height = height;
this.quote = quote;
+ this.caption = caption;
}
@Nullable
@@ -126,4 +130,8 @@ public abstract class Attachment {
public boolean isQuote() {
return quote;
}
+
+ public @Nullable String getCaption() {
+ return caption;
+ }
}
diff --git a/src/org/thoughtcrime/securesms/attachments/DatabaseAttachment.java b/src/org/thoughtcrime/securesms/attachments/DatabaseAttachment.java
index ba57be3752..f293991d25 100644
--- a/src/org/thoughtcrime/securesms/attachments/DatabaseAttachment.java
+++ b/src/org/thoughtcrime/securesms/attachments/DatabaseAttachment.java
@@ -17,9 +17,9 @@ public class DatabaseAttachment extends Attachment {
String contentType, int transferProgress, long size,
String fileName, String location, String key, String relay,
byte[] digest, String fastPreflightId, boolean voiceNote,
- int width, int height, boolean quote)
+ int width, int height, boolean quote, @Nullable String caption)
{
- super(contentType, transferProgress, size, fileName, location, key, relay, digest, fastPreflightId, voiceNote, width, height, quote);
+ super(contentType, transferProgress, size, fileName, location, key, relay, digest, fastPreflightId, voiceNote, width, height, quote, caption);
this.attachmentId = attachmentId;
this.hasData = hasData;
this.hasThumbnail = hasThumbnail;
diff --git a/src/org/thoughtcrime/securesms/attachments/MmsNotificationAttachment.java b/src/org/thoughtcrime/securesms/attachments/MmsNotificationAttachment.java
index 0011af6c92..2983ce4886 100644
--- a/src/org/thoughtcrime/securesms/attachments/MmsNotificationAttachment.java
+++ b/src/org/thoughtcrime/securesms/attachments/MmsNotificationAttachment.java
@@ -10,7 +10,7 @@ import org.thoughtcrime.securesms.database.MmsDatabase;
public class MmsNotificationAttachment extends Attachment {
public MmsNotificationAttachment(int status, long size) {
- super("application/mms", getTransferStateFromStatus(status), size, null, null, null, null, null, null, false, 0, 0, false);
+ super("application/mms", getTransferStateFromStatus(status), size, null, null, null, null, null, null, false, 0, 0, false, null);
}
@Nullable
diff --git a/src/org/thoughtcrime/securesms/attachments/PointerAttachment.java b/src/org/thoughtcrime/securesms/attachments/PointerAttachment.java
index 19427c5862..aba842f44b 100644
--- a/src/org/thoughtcrime/securesms/attachments/PointerAttachment.java
+++ b/src/org/thoughtcrime/securesms/attachments/PointerAttachment.java
@@ -19,9 +19,9 @@ public class PointerAttachment extends Attachment {
@Nullable String fileName, @NonNull String location,
@Nullable String key, @Nullable String relay,
@Nullable byte[] digest, boolean voiceNote,
- int width, int height)
+ int width, int height, @Nullable String caption)
{
- super(contentType, transferState, size, fileName, location, key, relay, digest, null, voiceNote, width, height, false);
+ super(contentType, transferState, size, fileName, location, key, relay, digest, null, voiceNote, width, height, false, caption);
}
@Nullable
@@ -87,7 +87,8 @@ public class PointerAttachment extends Attachment {
pointer.get().asPointer().getDigest().orNull(),
pointer.get().asPointer().getVoiceNote(),
pointer.get().asPointer().getWidth(),
- pointer.get().asPointer().getHeight()));
+ pointer.get().asPointer().getHeight(),
+ pointer.get().asPointer().getCaption().orNull()));
}
@@ -104,6 +105,7 @@ public class PointerAttachment extends Attachment {
thumbnail != null ? thumbnail.asPointer().getDigest().orNull() : null,
false,
thumbnail != null ? thumbnail.asPointer().getWidth() : 0,
- thumbnail != null ? thumbnail.asPointer().getHeight() : 0));
+ thumbnail != null ? thumbnail.asPointer().getHeight() : 0,
+ thumbnail != null ? thumbnail.asPointer().getCaption().orNull() : null));
}
}
diff --git a/src/org/thoughtcrime/securesms/attachments/UriAttachment.java b/src/org/thoughtcrime/securesms/attachments/UriAttachment.java
index 72c4f365ed..bea12e15ac 100644
--- a/src/org/thoughtcrime/securesms/attachments/UriAttachment.java
+++ b/src/org/thoughtcrime/securesms/attachments/UriAttachment.java
@@ -10,17 +10,17 @@ public class UriAttachment extends Attachment {
private final @Nullable Uri thumbnailUri;
public UriAttachment(@NonNull Uri uri, @NonNull String contentType, int transferState, long size,
- @Nullable String fileName, boolean voiceNote, boolean quote)
+ @Nullable String fileName, boolean voiceNote, boolean quote, @Nullable String caption)
{
- this(uri, uri, contentType, transferState, size, 0, 0, fileName, null, voiceNote, quote);
+ this(uri, uri, contentType, transferState, size, 0, 0, fileName, null, voiceNote, quote, caption);
}
public UriAttachment(@NonNull Uri dataUri, @Nullable Uri thumbnailUri,
@NonNull String contentType, int transferState, long size, int width, int height,
@Nullable String fileName, @Nullable String fastPreflightId,
- boolean voiceNote, boolean quote)
+ boolean voiceNote, boolean quote, @Nullable String caption)
{
- super(contentType, transferState, size, fileName, null, null, null, null, fastPreflightId, voiceNote, width, height, quote);
+ super(contentType, transferState, size, fileName, null, null, null, null, fastPreflightId, voiceNote, width, height, quote, caption);
this.dataUri = dataUri;
this.thumbnailUri = thumbnailUri;
}
diff --git a/src/org/thoughtcrime/securesms/components/AlbumThumbnailView.java b/src/org/thoughtcrime/securesms/components/AlbumThumbnailView.java
new file mode 100644
index 0000000000..c4286fe18f
--- /dev/null
+++ b/src/org/thoughtcrime/securesms/components/AlbumThumbnailView.java
@@ -0,0 +1,159 @@
+package org.thoughtcrime.securesms.components;
+
+import android.content.Context;
+import android.support.annotation.ColorInt;
+import android.support.annotation.IdRes;
+import android.util.AttributeSet;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+import android.widget.TextView;
+
+import org.thoughtcrime.securesms.R;
+import org.thoughtcrime.securesms.mms.GlideRequests;
+import org.thoughtcrime.securesms.mms.Slide;
+import org.thoughtcrime.securesms.mms.SlideClickListener;
+import org.thoughtcrime.securesms.mms.SlidesClickedListener;
+import org.thoughtcrime.securesms.util.views.Stub;
+
+import java.util.List;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+public class AlbumThumbnailView extends FrameLayout {
+
+ private @Nullable SlideClickListener thumbnailClickListener;
+ private @Nullable SlidesClickedListener downloadClickListener;
+
+ private int currentSizeClass;
+
+ private ViewGroup albumCellContainer;
+ private Stub transferControls;
+
+ private final SlideClickListener defaultThumbnailClickListener = (v, slide) -> {
+ if (thumbnailClickListener != null) {
+ thumbnailClickListener.onClick(v, slide);
+ }
+ };
+
+ private final OnLongClickListener defaultLongClickListener = v -> this.performLongClick();
+
+ public AlbumThumbnailView(@NonNull @android.support.annotation.NonNull Context context) {
+ super(context);
+ initialize();
+ }
+
+ public AlbumThumbnailView(@NonNull @android.support.annotation.NonNull Context context, @Nullable @android.support.annotation.Nullable AttributeSet attrs) {
+ super(context, attrs);
+ initialize();
+ }
+
+ private void initialize() {
+ inflate(getContext(), R.layout.album_thumbnail_view, this);
+
+ albumCellContainer = findViewById(R.id.album_cell_container);
+ transferControls = new Stub<>(findViewById(R.id.album_transfer_controls_stub));
+ }
+
+ public void setSlides(@NonNull GlideRequests glideRequests, @NonNull List slides, boolean showControls) {
+ if (slides.size() < 2) {
+ throw new IllegalStateException("Provided less than two slides.");
+ }
+
+ if (showControls) {
+ transferControls.get().setShowDownloadText(true);
+ transferControls.get().setSlides(slides);
+ transferControls.get().setDownloadClickListener(v -> {
+ if (downloadClickListener != null) {
+ downloadClickListener.onClick(v, slides);
+ }
+ });
+ } else {
+ if (transferControls.resolved()) {
+ transferControls.get().setVisibility(GONE);
+ }
+ }
+
+ int sizeClass = sizeClass(slides.size());
+
+ if (sizeClass != currentSizeClass) {
+ inflateLayout(sizeClass);
+ currentSizeClass = sizeClass;
+ }
+
+ showSlides(glideRequests, slides);
+ }
+
+ public void setCellBackgroundColor(@ColorInt int color) {
+ ViewGroup cellRoot = findViewById(R.id.album_thumbnail_root);
+
+ if (cellRoot != null) {
+ for (int i = 0; i < cellRoot.getChildCount(); i++) {
+ cellRoot.getChildAt(i).setBackgroundColor(color);
+ }
+ }
+ }
+
+ public void setThumbnailClickListener(@Nullable SlideClickListener listener) {
+ thumbnailClickListener = listener;
+ }
+
+ public void setDownloadClickListener(@Nullable SlidesClickedListener listener) {
+ downloadClickListener = listener;
+ }
+
+ private void inflateLayout(int sizeClass) {
+ albumCellContainer.removeAllViews();
+
+ switch (sizeClass) {
+ case 2:
+ inflate(getContext(), R.layout.album_thumbnail_2, albumCellContainer);
+ break;
+ case 3:
+ inflate(getContext(), R.layout.album_thumbnail_3, albumCellContainer);
+ break;
+ case 4:
+ inflate(getContext(), R.layout.album_thumbnail_4, albumCellContainer);
+ break;
+ case 5:
+ inflate(getContext(), R.layout.album_thumbnail_5, albumCellContainer);
+ break;
+ default:
+ inflate(getContext(), R.layout.album_thumbnail_many, albumCellContainer);
+ break;
+ }
+ }
+
+ private void showSlides(@NonNull GlideRequests glideRequests, @NonNull List slides) {
+ setSlide(glideRequests, slides.get(0), R.id.album_cell_1);
+ setSlide(glideRequests, slides.get(1), R.id.album_cell_2);
+
+ if (slides.size() >= 3) {
+ setSlide(glideRequests, slides.get(2), R.id.album_cell_3);
+ }
+
+ if (slides.size() >= 4) {
+ setSlide(glideRequests, slides.get(3), R.id.album_cell_4);
+ }
+
+ if (slides.size() >= 5) {
+ setSlide(glideRequests, slides.get(4), R.id.album_cell_5);
+ }
+
+ if (slides.size() > 5) {
+ TextView text = findViewById(R.id.album_cell_overflow_text);
+ text.setText(getContext().getString(R.string.AlbumThumbnailView_plus, slides.size() - 5));
+ }
+ }
+
+ private void setSlide(@NonNull GlideRequests glideRequests, @NonNull Slide slide, @IdRes int id) {
+ ThumbnailView cell = findViewById(id);
+ cell.setImageResource(glideRequests, slide, false, false);
+ cell.setThumbnailClickListener(defaultThumbnailClickListener);
+ cell.setOnLongClickListener(defaultLongClickListener);
+ }
+
+ private int sizeClass(int size) {
+ return Math.min(size, 6);
+ }
+}
diff --git a/src/org/thoughtcrime/securesms/components/ConversationItemThumbnail.java b/src/org/thoughtcrime/securesms/components/ConversationItemThumbnail.java
index 7b2973dd3a..048780897f 100644
--- a/src/org/thoughtcrime/securesms/components/ConversationItemThumbnail.java
+++ b/src/org/thoughtcrime/securesms/components/ConversationItemThumbnail.java
@@ -3,11 +3,10 @@ package org.thoughtcrime.securesms.components;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
-import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.RectF;
-import android.net.Uri;
+import android.support.annotation.ColorInt;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.UiThread;
@@ -16,40 +15,36 @@ import android.widget.FrameLayout;
import android.widget.ImageView;
import org.thoughtcrime.securesms.R;
+import org.thoughtcrime.securesms.attachments.Attachment;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.mms.Slide;
import org.thoughtcrime.securesms.mms.SlideClickListener;
+import org.thoughtcrime.securesms.mms.SlidesClickedListener;
import org.thoughtcrime.securesms.util.ThemeUtil;
+import java.util.List;
+
public class ConversationItemThumbnail extends FrameLayout {
private static final String TAG = ConversationItemThumbnail.class.getSimpleName();
- private static final Paint LIGHT_THEME_OUTLINE_PAINT = new Paint();
- private static final Paint DARK_THEME_OUTLINE_PAINT = new Paint();
-
- static {
- LIGHT_THEME_OUTLINE_PAINT.setColor(Color.argb((int) (255 * 0.2), 0, 0, 0));
- LIGHT_THEME_OUTLINE_PAINT.setStyle(Paint.Style.STROKE);
- LIGHT_THEME_OUTLINE_PAINT.setStrokeWidth(1f);
- LIGHT_THEME_OUTLINE_PAINT.setAntiAlias(true);
-
- DARK_THEME_OUTLINE_PAINT.setColor(Color.argb((int) (255 * 0.2), 255, 255, 255));
- DARK_THEME_OUTLINE_PAINT.setStyle(Paint.Style.STROKE);
- DARK_THEME_OUTLINE_PAINT.setStrokeWidth(1f);
- DARK_THEME_OUTLINE_PAINT.setAntiAlias(true);
- }
-
- private final float[] radii = new float[8];
- private final RectF bounds = new RectF();
- private final Path corners = new Path();
+ private final float[] radii = new float[8];
+ private final RectF bounds = new RectF();
+ private final Path corners = new Path();
private ThumbnailView thumbnail;
+ private AlbumThumbnailView album;
private ImageView shade;
private ConversationItemFooter footer;
- private Paint outlinePaint;
private CornerMask cornerMask;
+ private final Paint outlinePaint = new Paint();
+ {
+ outlinePaint.setStyle(Paint.Style.STROKE);
+ outlinePaint.setStrokeWidth(1f);
+ outlinePaint.setAntiAlias(true);
+ }
+
public ConversationItemThumbnail(Context context) {
super(context);
init(null);
@@ -68,13 +63,13 @@ public class ConversationItemThumbnail extends FrameLayout {
private void init(@Nullable AttributeSet attrs) {
inflate(getContext(), R.layout.conversation_item_thumbnail, this);
- this.thumbnail = findViewById(R.id.conversation_thumbnail_image);
- this.shade = findViewById(R.id.conversation_thumbnail_shade);
- this.footer = findViewById(R.id.conversation_thumbnail_footer);
- this.outlinePaint = ThemeUtil.isDarkTheme(getContext()) ? DARK_THEME_OUTLINE_PAINT : LIGHT_THEME_OUTLINE_PAINT;
- this.cornerMask = new CornerMask(this);
+ outlinePaint.setColor(ThemeUtil.getThemedColor(getContext(), R.attr.conversation_item_image_outline_color));
- setTouchDelegate(thumbnail.getTouchDelegate());
+ this.thumbnail = findViewById(R.id.conversation_thumbnail_image);
+ this.album = findViewById(R.id.conversation_thumbnail_album);
+ this.shade = findViewById(R.id.conversation_thumbnail_shade);
+ this.footer = findViewById(R.id.conversation_thumbnail_footer);
+ this.cornerMask = new CornerMask(this);
if (attrs != null) {
TypedArray typedArray = getContext().getTheme().obtainStyledAttributes(attrs, R.styleable.ConversationItemThumbnail, 0, 0);
@@ -99,32 +94,37 @@ public class ConversationItemThumbnail extends FrameLayout {
cornerMask.mask(canvas);
}
- final float halfStrokeWidth = outlinePaint.getStrokeWidth() / 2;
+ if (album.getVisibility() != VISIBLE) {
+ final float halfStrokeWidth = outlinePaint.getStrokeWidth() / 2;
- bounds.left = halfStrokeWidth;
- bounds.top = halfStrokeWidth;
- bounds.right = canvas.getWidth() - halfStrokeWidth;
- bounds.bottom = canvas.getHeight() - halfStrokeWidth;
+ bounds.left = halfStrokeWidth;
+ bounds.top = halfStrokeWidth;
+ bounds.right = canvas.getWidth() - halfStrokeWidth;
+ bounds.bottom = canvas.getHeight() - halfStrokeWidth;
- corners.reset();
- corners.addRoundRect(bounds, radii, Path.Direction.CW);
+ corners.reset();
+ corners.addRoundRect(bounds, radii, Path.Direction.CW);
- canvas.drawPath(corners, outlinePaint);
+ canvas.drawPath(corners, outlinePaint);
+ }
}
@Override
public void setFocusable(boolean focusable) {
thumbnail.setFocusable(focusable);
+ album.setFocusable(focusable);
}
@Override
public void setClickable(boolean clickable) {
thumbnail.setClickable(clickable);
+ album.setClickable(clickable);
}
@Override
public void setOnLongClickListener(@Nullable OnLongClickListener l) {
thumbnail.setOnLongClickListener(l);
+ album.setOnLongClickListener(l);
}
public void showShade(boolean show) {
@@ -146,37 +146,38 @@ public class ConversationItemThumbnail extends FrameLayout {
}
@UiThread
- public void setImageResource(@NonNull GlideRequests glideRequests, @NonNull Slide slide,
+ public void setImageResource(@NonNull GlideRequests glideRequests, @NonNull List slides,
boolean showControls, boolean isPreview)
{
- thumbnail.setImageResource(glideRequests, slide, showControls, isPreview);
+ if (slides.size() == 1) {
+ thumbnail.setVisibility(VISIBLE);
+ album.setVisibility(GONE);
+
+ Attachment attachment = slides.get(0).asAttachment();
+ thumbnail.setImageResource(glideRequests, slides.get(0), showControls, isPreview, attachment.getWidth(), attachment.getHeight());
+ setTouchDelegate(thumbnail.getTouchDelegate());
+ } else {
+ thumbnail.setVisibility(GONE);
+ album.setVisibility(VISIBLE);
+
+ album.setSlides(glideRequests, slides, showControls);
+ setTouchDelegate(album.getTouchDelegate());
+ }
}
- @UiThread
- public void setImageResource(@NonNull GlideRequests glideRequests, @NonNull Slide slide,
- boolean showControls, boolean isPreview, int naturalWidth,
- int naturalHeight)
- {
- thumbnail.setImageResource(glideRequests, slide, showControls, isPreview, naturalWidth, naturalHeight);
- }
-
- public void setImageResource(@NonNull GlideRequests glideRequests, @NonNull Uri uri) {
- thumbnail.setImageResource(glideRequests, uri);
+ public void setConversationColor(@ColorInt int color) {
+ if (album.getVisibility() == VISIBLE) {
+ album.setCellBackgroundColor(color);
+ }
}
public void setThumbnailClickListener(SlideClickListener listener) {
thumbnail.setThumbnailClickListener(listener);
+ album.setThumbnailClickListener(listener);
}
- public void setDownloadClickListener(SlideClickListener listener) {
+ public void setDownloadClickListener(SlidesClickedListener listener) {
thumbnail.setDownloadClickListener(listener);
- }
-
- public void clear(GlideRequests glideRequests) {
- thumbnail.clear(glideRequests);
- }
-
- public void showProgressSpinner() {
- thumbnail.showProgressSpinner();
+ album.setDownloadClickListener(listener);
}
}
diff --git a/src/org/thoughtcrime/securesms/components/MaxHeightScrollView.java b/src/org/thoughtcrime/securesms/components/MaxHeightScrollView.java
new file mode 100644
index 0000000000..7f129e7d0d
--- /dev/null
+++ b/src/org/thoughtcrime/securesms/components/MaxHeightScrollView.java
@@ -0,0 +1,42 @@
+package org.thoughtcrime.securesms.components;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.support.annotation.Nullable;
+import android.util.AttributeSet;
+import android.widget.ScrollView;
+
+import org.thoughtcrime.securesms.R;
+
+public class MaxHeightScrollView extends ScrollView {
+
+ private int maxHeight = -1;
+
+ public MaxHeightScrollView(Context context) {
+ super(context);
+ initialize(null);
+ }
+
+ public MaxHeightScrollView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ initialize(attrs);
+ }
+
+ private void initialize(@Nullable AttributeSet attrs) {
+ if (attrs != null) {
+ TypedArray typedArray = getContext().getTheme().obtainStyledAttributes(attrs, R.styleable.MaxHeightScrollView, 0, 0);
+
+ maxHeight = typedArray.getDimensionPixelOffset(R.styleable.MaxHeightScrollView_scrollView_maxHeight, -1);
+
+ typedArray.recycle();
+ }
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ if (maxHeight >= 0) {
+ heightMeasureSpec = MeasureSpec.makeMeasureSpec(maxHeight, MeasureSpec.AT_MOST);
+ }
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ }
+}
diff --git a/src/org/thoughtcrime/securesms/components/MediaView.java b/src/org/thoughtcrime/securesms/components/MediaView.java
index d8d809ff53..a4f17c9a03 100644
--- a/src/org/thoughtcrime/securesms/components/MediaView.java
+++ b/src/org/thoughtcrime/securesms/components/MediaView.java
@@ -11,6 +11,7 @@ import android.util.AttributeSet;
import android.view.View;
import android.view.Window;
import android.widget.FrameLayout;
+import android.widget.TextView;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.mms.GlideRequests;
@@ -81,6 +82,19 @@ public class MediaView extends FrameLayout {
}
}
+ public void hideControls() {
+ if (this.videoView.resolved()){
+ this.videoView.get().hideControls();
+ }
+ }
+
+ public @Nullable View getPlaybackControls() {
+ if (this.videoView.resolved()){
+ return this.videoView.get().getControlView();
+ }
+ return null;
+ }
+
public void cleanup() {
this.imageView.cleanup();
if (this.videoView.resolved()) {
diff --git a/src/org/thoughtcrime/securesms/components/ThumbnailView.java b/src/org/thoughtcrime/securesms/components/ThumbnailView.java
index ad8273cd8d..b7e61f3ae4 100644
--- a/src/org/thoughtcrime/securesms/components/ThumbnailView.java
+++ b/src/org/thoughtcrime/securesms/components/ThumbnailView.java
@@ -27,12 +27,14 @@ import org.thoughtcrime.securesms.mms.GlideRequest;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.mms.Slide;
import org.thoughtcrime.securesms.mms.SlideClickListener;
+import org.thoughtcrime.securesms.mms.SlidesClickedListener;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.ViewUtil;
import org.thoughtcrime.securesms.util.concurrent.ListenableFuture;
import org.thoughtcrime.securesms.util.concurrent.SettableFuture;
import org.whispersystems.libsignal.util.guava.Optional;
+import java.util.Collections;
import java.util.Locale;
import static com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions.withCrossFade;
@@ -49,6 +51,7 @@ public class ThumbnailView extends FrameLayout {
private ImageView image;
private View playOverlay;
+ private View captionIcon;
private OnClickListener parentClickListener;
private final int[] dimens = new int[2];
@@ -57,7 +60,7 @@ public class ThumbnailView extends FrameLayout {
private Optional transferControls = Optional.absent();
private SlideClickListener thumbnailClickListener = null;
- private SlideClickListener downloadClickListener = null;
+ private SlidesClickedListener downloadClickListener = null;
private Slide slide = null;
private int radius;
@@ -77,6 +80,7 @@ public class ThumbnailView extends FrameLayout {
this.image = findViewById(R.id.thumbnail_image);
this.playOverlay = findViewById(R.id.play_overlay);
+ this.captionIcon = findViewById(R.id.thumbnail_caption_icon);
super.setOnClickListener(new ThumbnailClickDispatcher());
if (attrs != null) {
@@ -230,8 +234,8 @@ public class ThumbnailView extends FrameLayout {
@UiThread
public ListenableFuture setImageResource(@NonNull GlideRequests glideRequests, @NonNull Slide slide,
- boolean showControls, boolean isPreview, int naturalWidth,
- int naturalHeight)
+ boolean showControls, boolean isPreview,
+ int naturalWidth, int naturalHeight)
{
if (showControls) {
getTransferControls().setSlide(slide);
@@ -267,6 +271,8 @@ public class ThumbnailView extends FrameLayout {
this.slide = slide;
+ this.captionIcon.setVisibility(slide.getCaption().isPresent() ? VISIBLE : GONE);
+
dimens[WIDTH] = naturalWidth;
dimens[HEIGHT] = naturalHeight;
invalidate();
@@ -302,7 +308,7 @@ public class ThumbnailView extends FrameLayout {
this.thumbnailClickListener = listener;
}
- public void setDownloadClickListener(SlideClickListener listener) {
+ public void setDownloadClickListener(SlidesClickedListener listener) {
this.downloadClickListener = listener;
}
@@ -342,8 +348,14 @@ public class ThumbnailView extends FrameLayout {
size[WIDTH] = getDefaultWidth();
size[HEIGHT] = getDefaultHeight();
}
- return request.override(size[WIDTH], size[HEIGHT])
- .transforms(fitting, new RoundedCorners(radius));
+
+ request = request.override(size[WIDTH], size[HEIGHT]);
+
+ if (radius > 0) {
+ return request.transforms(fitting, new RoundedCorners(radius));
+ } else {
+ return request.transforms(fitting);
+ }
}
private int getDefaultWidth() {
@@ -382,7 +394,7 @@ public class ThumbnailView extends FrameLayout {
public void onClick(View view) {
Log.i(TAG, "onClick() for download button");
if (downloadClickListener != null && slide != null) {
- downloadClickListener.onClick(view, slide);
+ downloadClickListener.onClick(view, Collections.singletonList(slide));
} else {
Log.w(TAG, "Received a download button click, but unable to execute it. slide: " + String.valueOf(slide) + " downloadClickListener: " + String.valueOf(downloadClickListener));
}
diff --git a/src/org/thoughtcrime/securesms/components/TransferControlView.java b/src/org/thoughtcrime/securesms/components/TransferControlView.java
index af43c1e2eb..63d40beab2 100644
--- a/src/org/thoughtcrime/securesms/components/TransferControlView.java
+++ b/src/org/thoughtcrime/securesms/components/TransferControlView.java
@@ -15,32 +15,42 @@ import android.view.ViewGroup;
import android.widget.FrameLayout;
import android.widget.TextView;
+import com.annimon.stream.Stream;
import com.nineoldandroids.animation.Animator;
import com.nineoldandroids.animation.ValueAnimator;
-import com.nineoldandroids.animation.ValueAnimator.AnimatorUpdateListener;
import com.pnikosis.materialishprogress.ProgressWheel;
import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
import org.thoughtcrime.securesms.R;
+import org.thoughtcrime.securesms.attachments.Attachment;
import org.thoughtcrime.securesms.database.AttachmentDatabase;
import org.thoughtcrime.securesms.events.PartProgressEvent;
+import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.mms.Slide;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.ViewUtil;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
public class TransferControlView extends FrameLayout {
private static final int TRANSITION_MS = 300;
- @Nullable private Slide slide;
- @Nullable private View current;
+ @Nullable private List slides;
+ @Nullable private View current;
private final ProgressWheel progressWheel;
- private final TextView downloadDetails;
+ private final View downloadDetails;
+ private final TextView downloadDetailsText;
private final int contractedWidth;
private final int expandedWidth;
+ private final Map downloadProgress;
+
public TransferControlView(Context context) {
this(context, null);
}
@@ -61,10 +71,12 @@ public class TransferControlView extends FrameLayout {
ViewUtil.setBackground(this, background);
setVisibility(GONE);
- this.progressWheel = ViewUtil.findById(this, R.id.progress_wheel);
- this.downloadDetails = ViewUtil.findById(this, R.id.download_details);
- this.contractedWidth = getResources().getDimensionPixelSize(R.dimen.transfer_controls_contracted_width);
- this.expandedWidth = getResources().getDimensionPixelSize(R.dimen.transfer_controls_expanded_width);
+ this.downloadProgress = new HashMap<>();
+ this.progressWheel = ViewUtil.findById(this, R.id.progress_wheel);
+ this.downloadDetails = ViewUtil.findById(this, R.id.download_details);
+ this.downloadDetailsText = ViewUtil.findById(this, R.id.download_details_text);
+ this.contractedWidth = getResources().getDimensionPixelSize(R.dimen.transfer_controls_contracted_width);
+ this.expandedWidth = getResources().getDimensionPixelSize(R.dimen.transfer_controls_expanded_width);
}
@Override
@@ -91,20 +103,54 @@ public class TransferControlView extends FrameLayout {
EventBus.getDefault().unregister(this);
}
- public void setSlide(final @NonNull Slide slide) {
- this.slide = slide;
- if (slide.getTransferState() == AttachmentDatabase.TRANSFER_PROGRESS_STARTED) {
- showProgressSpinner();
- } else if (slide.isPendingDownload()) {
- downloadDetails.setText(slide.getContentDescription());
- display(downloadDetails);
+ public void setSlide(final @NonNull Slide slides) {
+ setSlides(Collections.singletonList(slides));
+ }
+
+ public void setSlides(final @NonNull List slides) {
+ if (slides.isEmpty()) {
+ throw new IllegalArgumentException("Must provide at least one slide.");
+ }
+
+ this.slides = slides;
+
+ if (!isUpdateToExistingSet(slides)) {
+ downloadProgress.clear();
+ Stream.of(slides).forEach(s -> downloadProgress.put(s.asAttachment(), 0f));
} else {
- display(null);
+ for (Slide slide : slides) {
+ if (slide.asAttachment().getTransferState() == AttachmentDatabase.TRANSFER_PROGRESS_DONE) {
+ downloadProgress.put(slide.asAttachment(), 1f);
+ }
+ }
+ }
+
+ switch (getTransferState(slides)) {
+ case AttachmentDatabase.TRANSFER_PROGRESS_STARTED:
+ showProgressSpinner(calculateProgress(downloadProgress));
+ break;
+ case AttachmentDatabase.TRANSFER_PROGRESS_PENDING:
+ case AttachmentDatabase.TRANSFER_PROGRESS_FAILED:
+ downloadDetailsText.setText(getDownloadText(this.slides));
+ display(downloadDetails);
+ break;
+ default:
+ display(null);
+ break;
}
}
public void showProgressSpinner() {
- progressWheel.spin();
+ showProgressSpinner(calculateProgress(downloadProgress));
+ }
+
+ public void showProgressSpinner(float progress) {
+ if (progress == 0) {
+ progressWheel.spin();
+ } else {
+ progressWheel.setInstantProgress(progress);
+ }
+
display(progressWheel);
}
@@ -120,12 +166,51 @@ public class TransferControlView extends FrameLayout {
current.setVisibility(GONE);
}
current = null;
- slide = null;
+ slides = null;
+ }
+
+ public void setShowDownloadText(boolean showDownloadText) {
+ downloadDetailsText.setVisibility(showDownloadText ? VISIBLE : GONE);
+ }
+
+ private boolean isUpdateToExistingSet(@NonNull List slides) {
+ if (slides.size() != downloadProgress.size()) {
+ return false;
+ }
+
+ for (Slide slide : slides) {
+ if (!downloadProgress.containsKey(slide.asAttachment())) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ private int getTransferState(@NonNull List slides) {
+ int transferState = AttachmentDatabase.TRANSFER_PROGRESS_DONE;
+ for (Slide slide : slides) {
+ if (slide.getTransferState() == AttachmentDatabase.TRANSFER_PROGRESS_PENDING && transferState == AttachmentDatabase.TRANSFER_PROGRESS_DONE) {
+ transferState = slide.getTransferState();
+ } else {
+ transferState = Math.max(transferState, slide.getTransferState());
+ }
+ }
+ return transferState;
+ }
+
+ private String getDownloadText(@NonNull List slides) {
+ if (slides.size() == 1) {
+ return slides.get(0).getContentDescription();
+ } else {
+ int downloadCount = Stream.of(slides).reduce(0, (count, slide) -> slide.getTransferState() != AttachmentDatabase.TRANSFER_PROGRESS_DONE ? count + 1 : count);
+ return getContext().getResources().getQuantityString(R.plurals.TransferControlView_n_items, downloadCount, downloadCount);
+ }
}
private void display(@Nullable final View view) {
- final int sourceWidth = current == downloadDetails ? expandedWidth : contractedWidth;
- final int targetWidth = view == downloadDetails ? expandedWidth : contractedWidth;
+ final int sourceWidth = (current == downloadDetails && downloadDetailsText.getVisibility() == VISIBLE) ? expandedWidth : contractedWidth;
+ final int targetWidth = (view == downloadDetails && downloadDetailsText.getVisibility() == VISIBLE) ? expandedWidth : contractedWidth;
if (current == view || current == null) {
ViewGroup.LayoutParams layoutParams = getLayoutParams();
@@ -149,28 +234,31 @@ public class TransferControlView extends FrameLayout {
private Animator getWidthAnimator(final int from, final int to) {
final ValueAnimator anim = ValueAnimator.ofInt(from, to);
- anim.addUpdateListener(new AnimatorUpdateListener() {
- @Override
- public void onAnimationUpdate(ValueAnimator animation) {
- final int val = (Integer)animation.getAnimatedValue();
- final ViewGroup.LayoutParams layoutParams = getLayoutParams();
- layoutParams.width = val;
- setLayoutParams(layoutParams);
- }
+ anim.addUpdateListener(animation -> {
+ final int val = (Integer)animation.getAnimatedValue();
+ final ViewGroup.LayoutParams layoutParams = getLayoutParams();
+ layoutParams.width = val;
+ setLayoutParams(layoutParams);
});
anim.setInterpolator(new FastOutSlowInInterpolator());
anim.setDuration(TRANSITION_MS);
return anim;
}
+ private float calculateProgress(@NonNull Map downloadProgress) {
+ float totalProgress = 0;
+ for (float progress : downloadProgress.values()) {
+ totalProgress += progress / downloadProgress.size();
+ }
+ return totalProgress;
+ }
+
@Subscribe(sticky = true, threadMode = ThreadMode.ASYNC)
public void onEventAsync(final PartProgressEvent event) {
- if (this.slide != null && event.attachment.equals(this.slide.asAttachment())) {
- Util.runOnMain(new Runnable() {
- @Override
- public void run() {
- progressWheel.setInstantProgress(((float)event.progress) / event.total);
- }
+ if (downloadProgress.containsKey(event.attachment)) {
+ Util.runOnMain(() -> {
+ downloadProgress.put(event.attachment, ((float)event.progress) / event.total);
+ progressWheel.setInstantProgress(calculateProgress(downloadProgress));
});
}
}
diff --git a/src/org/thoughtcrime/securesms/contactshare/Contact.java b/src/org/thoughtcrime/securesms/contactshare/Contact.java
index ec9028ba9b..39d6f1cd39 100644
--- a/src/org/thoughtcrime/securesms/contactshare/Contact.java
+++ b/src/org/thoughtcrime/securesms/contactshare/Contact.java
@@ -642,7 +642,7 @@ public class Contact implements Parcelable {
private static Attachment attachmentFromUri(@Nullable Uri uri) {
if (uri == null) return null;
- return new UriAttachment(uri, MediaUtil.IMAGE_JPEG, AttachmentDatabase.TRANSFER_PROGRESS_DONE, 0, null, false, false);
+ return new UriAttachment(uri, MediaUtil.IMAGE_JPEG, AttachmentDatabase.TRANSFER_PROGRESS_DONE, 0, null, false, false, null);
}
@Override
diff --git a/src/org/thoughtcrime/securesms/database/AttachmentDatabase.java b/src/org/thoughtcrime/securesms/database/AttachmentDatabase.java
index 86efcb027a..7fe79dc8f9 100644
--- a/src/org/thoughtcrime/securesms/database/AttachmentDatabase.java
+++ b/src/org/thoughtcrime/securesms/database/AttachmentDatabase.java
@@ -98,6 +98,7 @@ public class AttachmentDatabase extends Database {
private static final String THUMBNAIL_RANDOM = "thumbnail_random";
static final String WIDTH = "width";
static final String HEIGHT = "height";
+ static final String CAPTION = "caption";
public static final String DIRECTORY = "parts";
@@ -113,7 +114,8 @@ public class AttachmentDatabase extends Database {
CONTENT_LOCATION, DATA, THUMBNAIL, TRANSFER_STATE,
SIZE, FILE_NAME, THUMBNAIL, THUMBNAIL_ASPECT_RATIO,
UNIQUE_ID, DIGEST, FAST_PREFLIGHT_ID, VOICE_NOTE,
- QUOTE, DATA_RANDOM, THUMBNAIL_RANDOM, WIDTH, HEIGHT};
+ QUOTE, DATA_RANDOM, THUMBNAIL_RANDOM, WIDTH, HEIGHT,
+ CAPTION };
public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " (" + ROW_ID + " INTEGER PRIMARY KEY, " +
MMS_ID + " INTEGER, " + "seq" + " INTEGER DEFAULT 0, " +
@@ -125,7 +127,8 @@ public class AttachmentDatabase extends Database {
FILE_NAME + " TEXT, " + THUMBNAIL + " TEXT, " + THUMBNAIL_ASPECT_RATIO + " REAL, " +
UNIQUE_ID + " INTEGER NOT NULL, " + DIGEST + " BLOB, " + FAST_PREFLIGHT_ID + " TEXT, " +
VOICE_NOTE + " INTEGER DEFAULT 0, " + DATA_RANDOM + " BLOB, " + THUMBNAIL_RANDOM + " BLOB, " +
- QUOTE + " INTEGER DEFAULT 0, " + WIDTH + " INTEGER DEFAULT 0, " + HEIGHT + " INTEGER DEFAULT 0);";
+ QUOTE + " INTEGER DEFAULT 0, " + WIDTH + " INTEGER DEFAULT 0, " + HEIGHT + " INTEGER DEFAULT 0, " +
+ CAPTION + " TEXT DEFAULT NULL);";
public static final String[] CREATE_INDEXS = {
"CREATE INDEX IF NOT EXISTS part_mms_id_index ON " + TABLE_NAME + " (" + MMS_ID + ");",
@@ -416,7 +419,8 @@ public class AttachmentDatabase extends Database {
databaseAttachment.isVoiceNote(),
mediaStream.getWidth(),
mediaStream.getHeight(),
- databaseAttachment.isQuote());
+ databaseAttachment.isQuote(),
+ databaseAttachment.getCaption());
}
@@ -589,7 +593,8 @@ public class AttachmentDatabase extends Database {
object.getInt(VOICE_NOTE) == 1,
object.getInt(WIDTH),
object.getInt(HEIGHT),
- object.getInt(QUOTE) == 1));
+ object.getInt(QUOTE) == 1,
+ object.getString(CAPTION)));
}
}
@@ -612,7 +617,8 @@ public class AttachmentDatabase extends Database {
cursor.getInt(cursor.getColumnIndexOrThrow(VOICE_NOTE)) == 1,
cursor.getInt(cursor.getColumnIndexOrThrow(WIDTH)),
cursor.getInt(cursor.getColumnIndexOrThrow(HEIGHT)),
- cursor.getInt(cursor.getColumnIndexOrThrow(QUOTE)) == 1));
+ cursor.getInt(cursor.getColumnIndexOrThrow(QUOTE)) == 1,
+ cursor.getString(cursor.getColumnIndexOrThrow(CAPTION))));
}
} catch (JSONException e) {
throw new AssertionError(e);
@@ -650,6 +656,7 @@ public class AttachmentDatabase extends Database {
contentValues.put(WIDTH, attachment.getWidth());
contentValues.put(HEIGHT, attachment.getHeight());
contentValues.put(QUOTE, quote);
+ contentValues.put(CAPTION, attachment.getCaption());
if (dataInfo != null) {
contentValues.put(DATA, dataInfo.file.getAbsolutePath());
diff --git a/src/org/thoughtcrime/securesms/database/MediaDatabase.java b/src/org/thoughtcrime/securesms/database/MediaDatabase.java
index 6feb9731e8..ff35cead9b 100644
--- a/src/org/thoughtcrime/securesms/database/MediaDatabase.java
+++ b/src/org/thoughtcrime/securesms/database/MediaDatabase.java
@@ -35,6 +35,7 @@ public class MediaDatabase extends Database {
+ AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.WIDTH + ", "
+ AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.HEIGHT + ", "
+ AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.QUOTE + ", "
+ + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.CAPTION + ", "
+ AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.NAME + ", "
+ MmsDatabase.TABLE_NAME + "." + MmsDatabase.MESSAGE_BOX + ", "
+ MmsDatabase.TABLE_NAME + "." + MmsDatabase.DATE_SENT + ", "
@@ -84,7 +85,9 @@ public class MediaDatabase extends Database {
private final long date;
private final boolean outgoing;
- private MediaRecord(DatabaseAttachment attachment, @Nullable Address address, long date, boolean outgoing) {
+ // TODO: Make private again
+ public MediaRecord(DatabaseAttachment attachment, @Nullable Address address, long date, boolean outgoing) {
+// private MediaRecord(DatabaseAttachment attachment, @Nullable Address address, long date, boolean outgoing) {
this.attachment = attachment;
this.address = address;
this.date = date;
diff --git a/src/org/thoughtcrime/securesms/database/MmsDatabase.java b/src/org/thoughtcrime/securesms/database/MmsDatabase.java
index fbd1e52dfb..961a1316f5 100644
--- a/src/org/thoughtcrime/securesms/database/MmsDatabase.java
+++ b/src/org/thoughtcrime/securesms/database/MmsDatabase.java
@@ -163,7 +163,8 @@ public class MmsDatabase extends MessagingDatabase {
"'" + AttachmentDatabase.QUOTE + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.QUOTE + ", " +
"'" + AttachmentDatabase.CONTENT_DISPOSITION + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.CONTENT_DISPOSITION + ", " +
"'" + AttachmentDatabase.NAME + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.NAME + ", " +
- "'" + AttachmentDatabase.TRANSFER_STATE + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.TRANSFER_STATE +
+ "'" + AttachmentDatabase.TRANSFER_STATE + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.TRANSFER_STATE + ", " +
+ "'" + AttachmentDatabase.CAPTION + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.CAPTION +
")) AS " + AttachmentDatabase.ATTACHMENT_JSON_ALIAS,
};
@@ -715,7 +716,8 @@ public class MmsDatabase extends MessagingDatabase {
databaseAttachment.isVoiceNote(),
databaseAttachment.getWidth(),
databaseAttachment.getHeight(),
- databaseAttachment.isQuote()));
+ databaseAttachment.isQuote(),
+ databaseAttachment.getCaption()));
}
return insertMediaMessage(request.getBody(),
diff --git a/src/org/thoughtcrime/securesms/database/MmsSmsDatabase.java b/src/org/thoughtcrime/securesms/database/MmsSmsDatabase.java
index 7f72a0551e..2ce3cd079b 100644
--- a/src/org/thoughtcrime/securesms/database/MmsSmsDatabase.java
+++ b/src/org/thoughtcrime/securesms/database/MmsSmsDatabase.java
@@ -227,7 +227,8 @@ public class MmsSmsDatabase extends Database {
"'" + AttachmentDatabase.QUOTE + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.QUOTE + ", " +
"'" + AttachmentDatabase.CONTENT_DISPOSITION + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.CONTENT_DISPOSITION + ", " +
"'" + AttachmentDatabase.NAME + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.NAME + ", " +
- "'" + AttachmentDatabase.TRANSFER_STATE + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.TRANSFER_STATE +
+ "'" + AttachmentDatabase.TRANSFER_STATE + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.TRANSFER_STATE + ", " +
+ "'" + AttachmentDatabase.CAPTION + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.CAPTION +
")) AS " + AttachmentDatabase.ATTACHMENT_JSON_ALIAS,
SmsDatabase.BODY, MmsSmsColumns.READ, MmsSmsColumns.THREAD_ID,
SmsDatabase.TYPE, SmsDatabase.ADDRESS, SmsDatabase.ADDRESS_DEVICE_ID, SmsDatabase.SUBJECT, MmsDatabase.MESSAGE_TYPE,
@@ -326,6 +327,7 @@ public class MmsSmsDatabase extends Database {
mmsColumnsPresent.add(AttachmentDatabase.WIDTH);
mmsColumnsPresent.add(AttachmentDatabase.HEIGHT);
mmsColumnsPresent.add(AttachmentDatabase.QUOTE);
+ mmsColumnsPresent.add(AttachmentDatabase.CAPTION);
mmsColumnsPresent.add(AttachmentDatabase.CONTENT_DISPOSITION);
mmsColumnsPresent.add(AttachmentDatabase.NAME);
mmsColumnsPresent.add(AttachmentDatabase.TRANSFER_STATE);
diff --git a/src/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java b/src/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java
index 7acb168c17..447bdbe05e 100644
--- a/src/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java
+++ b/src/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java
@@ -57,8 +57,9 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
private static final int QUOTE_MISSING = 11;
private static final int NOTIFICATION_CHANNELS = 12;
private static final int SECRET_SENDER = 13;
+ private static final int ATTACHMENT_CAPTIONS = 14;
- private static final int DATABASE_VERSION = 13;
+ private static final int DATABASE_VERSION = 14;
private static final String DATABASE_NAME = "signal.db";
private final Context context;
@@ -294,6 +295,10 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
db.execSQL("ALTER TABLE sms ADD COLUMN unidentified INTEGER DEFAULT 0");
}
+ if (oldVersion < ATTACHMENT_CAPTIONS) {
+ db.execSQL("ALTER TABLE part ADD COLUMN caption TEXT DEFAULT NULL");
+ }
+
db.setTransactionSuccessful();
} finally {
db.endTransaction();
diff --git a/src/org/thoughtcrime/securesms/groups/GroupManager.java b/src/org/thoughtcrime/securesms/groups/GroupManager.java
index 9fee1eda13..355c3996a1 100644
--- a/src/org/thoughtcrime/securesms/groups/GroupManager.java
+++ b/src/org/thoughtcrime/securesms/groups/GroupManager.java
@@ -112,7 +112,7 @@ public class GroupManager {
if (avatar != null) {
Uri avatarUri = MemoryBlobProvider.getInstance().createSingleUseUri(avatar);
- avatarAttachment = new UriAttachment(avatarUri, MediaUtil.IMAGE_PNG, AttachmentDatabase.TRANSFER_PROGRESS_DONE, avatar.length, null, false, false);
+ avatarAttachment = new UriAttachment(avatarUri, MediaUtil.IMAGE_PNG, AttachmentDatabase.TRANSFER_PROGRESS_DONE, avatar.length, null, false, false, null);
}
OutgoingGroupMediaMessage outgoingMessage = new OutgoingGroupMediaMessage(groupRecipient, groupContext, avatarAttachment, System.currentTimeMillis(), 0, null, Collections.emptyList());
diff --git a/src/org/thoughtcrime/securesms/jobs/AttachmentDownloadJob.java b/src/org/thoughtcrime/securesms/jobs/AttachmentDownloadJob.java
index fe377665d4..b57a6f2d8f 100644
--- a/src/org/thoughtcrime/securesms/jobs/AttachmentDownloadJob.java
+++ b/src/org/thoughtcrime/securesms/jobs/AttachmentDownloadJob.java
@@ -92,6 +92,16 @@ public class AttachmentDownloadJob extends ContextJob implements InjectableType
@Override
public void onAdded() {
Log.i(TAG, "onAdded() messageId: " + messageId + " partRowId: " + partRowId + " partUniqueId: " + partUniqueId + " manual: " + manual);
+
+ final AttachmentDatabase database = DatabaseFactory.getAttachmentDatabase(context);
+ final AttachmentId attachmentId = new AttachmentId(partRowId, partUniqueId);
+ final DatabaseAttachment attachment = database.getAttachment(attachmentId);
+ final boolean pending = attachment != null && attachment.getTransferState() != AttachmentDatabase.TRANSFER_PROGRESS_DONE;
+
+ if (pending && (manual || AttachmentUtil.isAutoDownloadPermitted(context, attachment))) {
+ Log.i(TAG, "onAdded() Marking attachment progress as 'started'");
+ database.setTransferState(messageId, attachmentId, AttachmentDatabase.TRANSFER_PROGRESS_STARTED);
+ }
}
@Override
diff --git a/src/org/thoughtcrime/securesms/jobs/MmsDownloadJob.java b/src/org/thoughtcrime/securesms/jobs/MmsDownloadJob.java
index 1ec9512220..12e7a2c0d0 100644
--- a/src/org/thoughtcrime/securesms/jobs/MmsDownloadJob.java
+++ b/src/org/thoughtcrime/securesms/jobs/MmsDownloadJob.java
@@ -241,7 +241,7 @@ public class MmsDownloadJob extends ContextJob {
attachments.add(new UriAttachment(uri, Util.toIsoString(part.getContentType()),
AttachmentDatabase.TRANSFER_PROGRESS_DONE,
- part.getData().length, name, false, false));
+ part.getData().length, name, false, false, null));
}
}
}
diff --git a/src/org/thoughtcrime/securesms/jobs/PushSendJob.java b/src/org/thoughtcrime/securesms/jobs/PushSendJob.java
index 8fa6c3e100..11c52595f2 100644
--- a/src/org/thoughtcrime/securesms/jobs/PushSendJob.java
+++ b/src/org/thoughtcrime/securesms/jobs/PushSendJob.java
@@ -36,6 +36,7 @@ import java.io.IOException;
import java.io.InputStream;
import java.util.LinkedList;
import java.util.List;
+import java.util.UUID;
import java.util.concurrent.TimeUnit;
public abstract class PushSendJob extends SendJob {
@@ -120,6 +121,7 @@ public abstract class PushSendJob extends SendJob {
.withVoiceNote(attachment.isVoiceNote())
.withWidth(attachment.getWidth())
.withHeight(attachment.getHeight())
+ .withCaption(attachment.getCaption())
.withListener((total, progress) -> EventBus.getDefault().postSticky(new PartProgressEvent(attachment, total, progress)))
.build();
} catch (IOException ioe) {
diff --git a/src/org/thoughtcrime/securesms/mediapreview/AlbumRailAdapter.java b/src/org/thoughtcrime/securesms/mediapreview/AlbumRailAdapter.java
new file mode 100644
index 0000000000..9a82363931
--- /dev/null
+++ b/src/org/thoughtcrime/securesms/mediapreview/AlbumRailAdapter.java
@@ -0,0 +1,100 @@
+package org.thoughtcrime.securesms.mediapreview;
+
+import android.support.annotation.NonNull;
+import android.support.v7.widget.RecyclerView;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import org.thoughtcrime.securesms.R;
+import org.thoughtcrime.securesms.components.ThumbnailView;
+import org.thoughtcrime.securesms.database.MediaDatabase.MediaRecord;
+import org.thoughtcrime.securesms.mms.GlideRequests;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class AlbumRailAdapter extends RecyclerView.Adapter {
+
+ private final GlideRequests glideRequests;
+ private final List records;
+ private final RailItemClickedListener listener;
+
+ private int activePosition;
+
+ public AlbumRailAdapter(@NonNull GlideRequests glideRequests, @NonNull RailItemClickedListener listener) {
+ this.glideRequests = glideRequests;
+ this.records = new ArrayList<>();
+ this.listener = listener;
+
+ setHasStableIds(true);
+ }
+
+ @NonNull
+ @Override
+ public AlbumRailViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int i) {
+ return new AlbumRailViewHolder(LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.media_preview_album_rail_item, viewGroup, false));
+ }
+
+ @Override
+ public void onBindViewHolder(@NonNull AlbumRailViewHolder albumRailViewHolder, int i) {
+ albumRailViewHolder.bind(records.get(i), i == activePosition, glideRequests, listener, i - activePosition);
+ }
+
+ @Override
+ public void onViewRecycled(@NonNull AlbumRailViewHolder holder) {
+ holder.recycle();
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return records.get(position).getAttachment().getAttachmentId().getUniqueId();
+ }
+
+ @Override
+ public int getItemCount() {
+ return records.size();
+ }
+
+ public void setRecords(@NonNull List records, int activePosition) {
+ this.activePosition = activePosition;
+
+ this.records.clear();
+ this.records.addAll(records);
+
+ notifyDataSetChanged();
+ }
+
+ static class AlbumRailViewHolder extends RecyclerView.ViewHolder {
+
+ private final ThumbnailView image;
+
+ AlbumRailViewHolder(@NonNull View itemView) {
+ super(itemView);
+ image = (ThumbnailView) itemView;
+ }
+
+ void bind(@NonNull MediaRecord record, boolean isActive, @NonNull GlideRequests glideRequests,
+ @NonNull RailItemClickedListener railItemClickedListener, int distanceFromActive)
+ {
+ if (record.getAttachment().getThumbnailUri() != null) {
+ image.setImageResource(glideRequests, record.getAttachment().getThumbnailUri());
+ } else if (record.getAttachment().getDataUri() != null) {
+ image.setImageResource(glideRequests, record.getAttachment().getDataUri());
+ } else {
+ image.clear(glideRequests);
+ }
+
+ image.setBackgroundResource(isActive ? R.drawable.album_rail_item_background : 0);
+ image.setOnClickListener(v -> railItemClickedListener.onRailItemClicked(distanceFromActive));
+ }
+
+ void recycle() {
+ image.setOnClickListener(null);
+ }
+ }
+
+ public interface RailItemClickedListener {
+ void onRailItemClicked(int distanceFromActive);
+ }
+}
diff --git a/src/org/thoughtcrime/securesms/mediapreview/MediaPreviewViewModel.java b/src/org/thoughtcrime/securesms/mediapreview/MediaPreviewViewModel.java
new file mode 100644
index 0000000000..20e3027535
--- /dev/null
+++ b/src/org/thoughtcrime/securesms/mediapreview/MediaPreviewViewModel.java
@@ -0,0 +1,110 @@
+package org.thoughtcrime.securesms.mediapreview;
+
+import android.arch.lifecycle.LiveData;
+import android.arch.lifecycle.MutableLiveData;
+import android.arch.lifecycle.ViewModel;
+import android.content.Context;
+import android.database.Cursor;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+
+import org.thoughtcrime.securesms.database.MediaDatabase.MediaRecord;
+
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
+
+public class MediaPreviewViewModel extends ViewModel {
+
+ private final MutableLiveData previewData = new MutableLiveData<>();
+
+ private boolean leftIsRecent;
+
+ private @Nullable Cursor cursor;
+
+ public void setCursor(@Nullable Cursor cursor, boolean leftIsRecent) {
+ this.cursor = cursor;
+ this.leftIsRecent = leftIsRecent;
+ }
+
+ public void setActiveAlbumRailItem(@NonNull Context context, int activePosition) {
+ if (cursor == null) {
+ previewData.postValue(new PreviewData(Collections.emptyList(), null, 0));
+ return;
+ }
+
+ activePosition = getCursorPosition(activePosition);
+
+ cursor.moveToPosition(activePosition);
+
+ MediaRecord activeRecord = MediaRecord.from(context, cursor);
+ LinkedList rail = new LinkedList<>();
+
+ rail.add(activeRecord);
+
+ while (cursor.moveToPrevious()) {
+ MediaRecord record = MediaRecord.from(context, cursor);
+ if (record.getAttachment().getMmsId() == activeRecord.getAttachment().getMmsId()) {
+ rail.addFirst(record);
+ } else {
+ break;
+ }
+ }
+
+ cursor.moveToPosition(activePosition);
+
+ while (cursor.moveToNext()) {
+ MediaRecord record = MediaRecord.from(context, cursor);
+ if (record.getAttachment().getMmsId() == activeRecord.getAttachment().getMmsId()) {
+ rail.addLast(record);
+ } else {
+ break;
+ }
+ }
+
+ if (!leftIsRecent) {
+ Collections.reverse(rail);
+ }
+
+ previewData.postValue(new PreviewData(rail.size() > 1 ? rail : Collections.emptyList(),
+ activeRecord.getAttachment().getCaption(),
+ rail.indexOf(activeRecord)));
+ }
+
+ private int getCursorPosition(int position) {
+ if (cursor == null) {
+ return 0;
+ }
+
+ if (leftIsRecent) return position;
+ else return cursor.getCount() - 1 - position;
+ }
+
+ public LiveData getPreviewData() {
+ return previewData;
+ }
+
+ public static class PreviewData {
+ private final List albumThumbnails;
+ private final String caption;
+ private final int activePosition;
+
+ public PreviewData(@NonNull List albumThumbnails, @Nullable String caption, int activePosition) {
+ this.albumThumbnails = albumThumbnails;
+ this.caption = caption;
+ this.activePosition = activePosition;
+ }
+
+ public @NonNull List getAlbumThumbnails() {
+ return albumThumbnails;
+ }
+
+ public @Nullable String getCaption() {
+ return caption;
+ }
+
+ public int getActivePosition() {
+ return activePosition;
+ }
+ }
+}
diff --git a/src/org/thoughtcrime/securesms/mms/AttachmentManager.java b/src/org/thoughtcrime/securesms/mms/AttachmentManager.java
index 0d9ad9b730..8aa631d255 100644
--- a/src/org/thoughtcrime/securesms/mms/AttachmentManager.java
+++ b/src/org/thoughtcrime/securesms/mms/AttachmentManager.java
@@ -34,6 +34,7 @@ import android.provider.OpenableColumns;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.text.TextUtils;
+
import org.thoughtcrime.securesms.logging.Log;
import android.util.Pair;
import android.view.View;
@@ -493,6 +494,7 @@ public class AttachmentManager {
Intent intent = new Intent(context, MediaPreviewActivity.class);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
intent.putExtra(MediaPreviewActivity.SIZE_EXTRA, slide.asAttachment().getSize());
+ intent.putExtra(MediaPreviewActivity.CAPTION_EXTRA, slide.getCaption().orNull());
intent.putExtra(MediaPreviewActivity.OUTGOING_EXTRA, true);
intent.setDataAndType(slide.getUri(), slide.getContentType());
diff --git a/src/org/thoughtcrime/securesms/mms/AudioSlide.java b/src/org/thoughtcrime/securesms/mms/AudioSlide.java
index b3b5b56150..ef65cb3fcf 100644
--- a/src/org/thoughtcrime/securesms/mms/AudioSlide.java
+++ b/src/org/thoughtcrime/securesms/mms/AudioSlide.java
@@ -38,7 +38,7 @@ public class AudioSlide extends Slide {
}
public AudioSlide(Context context, Uri uri, long dataSize, String contentType, boolean voiceNote) {
- super(context, new UriAttachment(uri, null, contentType, AttachmentDatabase.TRANSFER_PROGRESS_STARTED, dataSize, 0, 0, null, null, voiceNote, false));
+ super(context, new UriAttachment(uri, null, contentType, AttachmentDatabase.TRANSFER_PROGRESS_STARTED, dataSize, 0, 0, null, null, voiceNote, false, null));
}
public AudioSlide(Context context, Attachment attachment) {
diff --git a/src/org/thoughtcrime/securesms/mms/Slide.java b/src/org/thoughtcrime/securesms/mms/Slide.java
index 22261ba505..3c258410b7 100644
--- a/src/org/thoughtcrime/securesms/mms/Slide.java
+++ b/src/org/thoughtcrime/securesms/mms/Slide.java
@@ -41,7 +41,6 @@ public abstract class Slide {
public Slide(@NonNull Context context, @NonNull Attachment attachment) {
this.context = context;
this.attachment = attachment;
-
}
public String getContentType() {
@@ -63,6 +62,11 @@ public abstract class Slide {
return Optional.absent();
}
+ @NonNull
+ public Optional getCaption() {
+ return Optional.fromNullable(attachment.getCaption());
+ }
+
@NonNull
public Optional getFileName() {
return Optional.fromNullable(attachment.getFileName());
@@ -112,7 +116,7 @@ public abstract class Slide {
getTransferState() == AttachmentDatabase.TRANSFER_PROGRESS_PENDING;
}
- public long getTransferState() {
+ public int getTransferState() {
return attachment.getTransferState();
}
@@ -152,7 +156,8 @@ public abstract class Slide {
fileName,
fastPreflightId,
voiceNote,
- quote);
+ quote,
+ null);
} catch (NoSuchAlgorithmException e) {
throw new AssertionError(e);
}
diff --git a/src/org/thoughtcrime/securesms/mms/SlideDeck.java b/src/org/thoughtcrime/securesms/mms/SlideDeck.java
index 46b6ac30dd..d81524a194 100644
--- a/src/org/thoughtcrime/securesms/mms/SlideDeck.java
+++ b/src/org/thoughtcrime/securesms/mms/SlideDeck.java
@@ -20,6 +20,8 @@ import android.content.Context;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
+import com.annimon.stream.Stream;
+
import org.thoughtcrime.securesms.attachments.Attachment;
import org.thoughtcrime.securesms.util.MediaUtil;
import org.whispersystems.libsignal.util.guava.Optional;
@@ -103,6 +105,10 @@ public class SlideDeck {
return null;
}
+ public @NonNull List getThumbnailSlides() {
+ return Stream.of(slides).filter(Slide::hasImage).toList();
+ }
+
public @Nullable AudioSlide getAudioSlide() {
for (Slide slide : slides) {
if (slide.hasAudio()) {
diff --git a/src/org/thoughtcrime/securesms/mms/SlidesClickedListener.java b/src/org/thoughtcrime/securesms/mms/SlidesClickedListener.java
new file mode 100644
index 0000000000..9e6914d2ee
--- /dev/null
+++ b/src/org/thoughtcrime/securesms/mms/SlidesClickedListener.java
@@ -0,0 +1,9 @@
+package org.thoughtcrime.securesms.mms;
+
+import android.view.View;
+
+import java.util.List;
+
+public interface SlidesClickedListener {
+ void onClick(View v, List slides);
+}
diff --git a/src/org/thoughtcrime/securesms/video/VideoPlayer.java b/src/org/thoughtcrime/securesms/video/VideoPlayer.java
index b309b3e2f2..bf6b59a7d5 100644
--- a/src/org/thoughtcrime/securesms/video/VideoPlayer.java
+++ b/src/org/thoughtcrime/securesms/video/VideoPlayer.java
@@ -21,6 +21,7 @@ import android.os.Build;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
+import android.view.View;
import android.view.Window;
import android.view.WindowManager;
import android.widget.FrameLayout;
@@ -46,6 +47,7 @@ import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
import com.google.android.exoplayer2.trackselection.TrackSelection;
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
import com.google.android.exoplayer2.trackselection.TrackSelector;
+import com.google.android.exoplayer2.ui.PlayerControlView;
import com.google.android.exoplayer2.ui.PlayerView;
import com.google.android.exoplayer2.ui.SimpleExoPlayerView;
import com.google.android.exoplayer2.upstream.BandwidthMeter;
@@ -70,6 +72,7 @@ public class VideoPlayer extends FrameLayout {
@Nullable private final PlayerView exoView;
@Nullable private SimpleExoPlayer exoPlayer;
+ @Nullable private PlayerControlView exoControls;
@Nullable private AttachmentServer attachmentServer;
@Nullable private Window window;
@@ -89,6 +92,8 @@ public class VideoPlayer extends FrameLayout {
if (Build.VERSION.SDK_INT >= 16) {
this.exoView = ViewUtil.findById(this, R.id.video_view);
this.videoView = null;
+ this.exoControls = new PlayerControlView(getContext());
+ this.exoControls.setShowTimeoutMs(-1);
} else {
this.videoView = ViewUtil.findById(this, R.id.video_view);
this.exoView = null;
@@ -111,6 +116,19 @@ public class VideoPlayer extends FrameLayout {
}
}
+ public void hideControls() {
+ if (this.exoView != null) {
+ this.exoView.hideController();
+ }
+ }
+
+ public @Nullable View getControlView() {
+ if (this.exoControls != null) {
+ return this.exoControls;
+ }
+ return null;
+ }
+
public void cleanup() {
if (this.attachmentServer != null) {
this.attachmentServer.stop();
@@ -137,6 +155,8 @@ public class VideoPlayer extends FrameLayout {
exoPlayer.addListener(new ExoPlayerListener(window));
//noinspection ConstantConditions
exoView.setPlayer(exoPlayer);
+ //noinspection ConstantConditions
+ exoControls.setPlayer(exoPlayer);
DefaultDataSourceFactory defaultDataSourceFactory = new DefaultDataSourceFactory(getContext(), "GenericUserAgent", null);
AttachmentDataSourceFactory attachmentDataSourceFactory = new AttachmentDataSourceFactory(getContext(), defaultDataSourceFactory, null);