Added support for multi-image receive.

This commit is contained in:
Greyson Parrelli 2018-11-08 23:33:37 -08:00
parent e665252b86
commit 47a10a0288
55 changed files with 1277 additions and 186 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 661 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 336 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 725 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle" >
<solid android:color="@color/signal_primary"/>
<corners android:radius="2dp"/>
</shape>

View File

@ -1,14 +1,17 @@
<?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">
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.exoplayer2.ui.PlayerView
android:id="@+id/video_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:gravity="center"/>
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"/>
</FrameLayout>

View File

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/album_thumbnail_root"
android:orientation="horizontal"
android:layout_width="300dp"
android:layout_height="150dp">
<org.thoughtcrime.securesms.components.ThumbnailView
android:id="@+id/album_cell_1"
android:layout_height="150dp"
android:layout_width="149dp"
app:thumbnail_radius="0dp"/>
<org.thoughtcrime.securesms.components.ThumbnailView
android:id="@+id/album_cell_2"
android:layout_height="150dp"
android:layout_width="149dp"
android:layout_gravity="right|end"
app:thumbnail_radius="0dp"/>
</FrameLayout>

View File

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/album_thumbnail_root"
android:layout_width="300dp"
android:layout_height="200dp">
<org.thoughtcrime.securesms.components.ThumbnailView
android:id="@+id/album_cell_1"
android:layout_height="200dp"
android:layout_width="199dp"
app:thumbnail_radius="0dp"/>
<org.thoughtcrime.securesms.components.ThumbnailView
android:id="@+id/album_cell_2"
android:layout_height="99dp"
android:layout_width="99dp"
android:layout_gravity="right|end|top"
app:thumbnail_radius="0dp"/>
<org.thoughtcrime.securesms.components.ThumbnailView
android:id="@+id/album_cell_3"
android:layout_height="99dp"
android:layout_width="99dp"
android:layout_gravity="right|end|bottom"
app:thumbnail_radius="0dp"/>
</FrameLayout>

View File

@ -0,0 +1,36 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/album_thumbnail_root"
android:layout_width="300dp"
android:layout_height="300dp">
<org.thoughtcrime.securesms.components.ThumbnailView
android:id="@+id/album_cell_1"
android:layout_height="149dp"
android:layout_width="149dp"
app:thumbnail_radius="0dp"/>
<org.thoughtcrime.securesms.components.ThumbnailView
android:id="@+id/album_cell_2"
android:layout_height="149dp"
android:layout_width="149dp"
android:layout_gravity="right|end|top"
app:thumbnail_radius="0dp"/>
<org.thoughtcrime.securesms.components.ThumbnailView
android:id="@+id/album_cell_3"
android:layout_height="149dp"
android:layout_width="149dp"
android:layout_gravity="left|start|bottom"
app:thumbnail_radius="0dp"/>
<org.thoughtcrime.securesms.components.ThumbnailView
android:id="@+id/album_cell_4"
android:layout_height="149dp"
android:layout_width="149dp"
android:layout_gravity="right|end|bottom"
app:thumbnail_radius="0dp"/>
</FrameLayout>

View File

@ -0,0 +1,43 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/album_thumbnail_root"
android:layout_width="300dp"
android:layout_height="250dp">
<org.thoughtcrime.securesms.components.ThumbnailView
android:id="@+id/album_cell_1"
android:layout_height="149dp"
android:layout_width="149dp"
app:thumbnail_radius="0dp"/>
<org.thoughtcrime.securesms.components.ThumbnailView
android:id="@+id/album_cell_2"
android:layout_height="149dp"
android:layout_width="149dp"
android:layout_gravity="right|end|top"
app:thumbnail_radius="0dp"/>
<org.thoughtcrime.securesms.components.ThumbnailView
android:id="@+id/album_cell_3"
android:layout_height="99dp"
android:layout_width="99dp"
android:layout_gravity="left|start|bottom"
app:thumbnail_radius="0dp"/>
<org.thoughtcrime.securesms.components.ThumbnailView
android:id="@+id/album_cell_4"
android:layout_height="99dp"
android:layout_width="99dp"
android:layout_gravity="center_horizontal|bottom"
app:thumbnail_radius="0dp"/>
<org.thoughtcrime.securesms.components.ThumbnailView
android:id="@+id/album_cell_5"
android:layout_height="99dp"
android:layout_width="99dp"
android:layout_gravity="right|end|bottom"
app:thumbnail_radius="0dp"/>
</FrameLayout>

View File

@ -0,0 +1,61 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/album_thumbnail_root"
android:layout_width="300dp"
android:layout_height="250dp">
<org.thoughtcrime.securesms.components.ThumbnailView
android:id="@+id/album_cell_1"
android:layout_height="149dp"
android:layout_width="149dp"
app:thumbnail_radius="0dp"/>
<org.thoughtcrime.securesms.components.ThumbnailView
android:id="@+id/album_cell_2"
android:layout_height="149dp"
android:layout_width="149dp"
android:layout_gravity="right|end|top"
app:thumbnail_radius="0dp"/>
<org.thoughtcrime.securesms.components.ThumbnailView
android:id="@+id/album_cell_3"
android:layout_height="99dp"
android:layout_width="99dp"
android:layout_gravity="left|start|bottom"
app:thumbnail_radius="0dp"/>
<org.thoughtcrime.securesms.components.ThumbnailView
android:id="@+id/album_cell_4"
android:layout_height="99dp"
android:layout_width="99dp"
android:layout_gravity="center_horizontal|bottom"
app:thumbnail_radius="0dp"/>
<FrameLayout
android:layout_width="99dp"
android:layout_height="99dp"
android:layout_gravity="right|end|bottom">
<org.thoughtcrime.securesms.components.ThumbnailView
android:id="@+id/album_cell_5"
android:layout_height="match_parent"
android:layout_width="match_parent"
android:layout_gravity="center_horizontal|bottom"
app:thumbnail_radius="0dp"/>
<TextView
android:id="@+id/album_cell_overflow_text"
android:layout_height="match_parent"
android:layout_width="match_parent"
android:gravity="center"
android:textSize="28dp"
android:textColor="@color/core_white"
android:background="@color/transparent_black_40"
tools:text="+2" />
</FrameLayout>
</FrameLayout>

View File

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<merge
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<FrameLayout
android:id="@+id/album_cell_container"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?conversation_background"/>
<ViewStub
android:id="@+id/album_transfer_controls_stub"
android:layout_width="70dp"
android:layout_height="70dp"
android:layout_gravity="center"
android:layout="@layout/transfer_controls_stub" />
</merge>

View File

@ -1,7 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<merge
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<org.thoughtcrime.securesms.components.ThumbnailView
android:id="@+id/conversation_thumbnail_image"
@ -12,8 +13,19 @@
android:longClickable="false"
android:scaleType="fitCenter"
android:contentDescription="@string/conversation_item__mms_image_description"
android:visibility="gone"
tools:visibility="visible"
app:thumbnail_radius="1dp"/>
<org.thoughtcrime.securesms.components.AlbumThumbnailView
android:id="@+id/conversation_thumbnail_album"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:clickable="false"
android:longClickable="false"
android:contentDescription="@string/conversation_item__mms_image_description"
android:visibility="gone"/>
<ImageView
android:id="@+id/conversation_thumbnail_shade"
android:layout_width="match_parent"

View File

@ -1,13 +1,67 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/gray95">
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/gray95">
<org.thoughtcrime.securesms.components.viewpager.HackyViewPager
android:id="@+id/media_pager"
android:id="@+id/media_pager"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clickable="true"/>
<LinearLayout
android:id="@+id/media_preview_details_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:orientation="vertical"
android:background="@drawable/image_shade"
android:gravity="bottom"
android:visibility="gone"
android:animateLayoutChanges="true">
<org.thoughtcrime.securesms.components.MaxHeightScrollView
android:id="@+id/media_preview_caption_container"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
android:layout_height="wrap_content"
android:paddingTop="32dp"
android:animateLayoutChanges="true"
app:scrollView_maxHeight="120dp">
<org.thoughtcrime.securesms.components.emoji.EmojiTextView
android:id="@+id/media_preview_caption"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:paddingBottom="8dp"
style="@style/Signal.Text.Body"
android:textColor="@color/core_white"
android:gravity="bottom"
tools:text="With great power comes great responsibility." />
</org.thoughtcrime.securesms.components.MaxHeightScrollView>
<android.support.v7.widget.RecyclerView
android:id="@+id/media_preview_album_rail"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginLeft="12dp"
android:layout_marginRight="12dp"
tools:layout_height="64dp"/>
<FrameLayout
android:id="@+id/media_preview_playback_controls_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:animateLayoutChanges="true"/>
</LinearLayout>
</FrameLayout>

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<org.thoughtcrime.securesms.components.ThumbnailView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/rail_item_image"
android:layout_width="48dp"
android:layout_height="48sp"
android:layout_margin="2dp"
android:padding="2dp"
android:background="@drawable/album_rail_item_background"
app:thumbnail_radius="2dp"/>

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.exoplayer2.ui.AspectRatioFrameLayout
android:id="@+id/exo_content_frame"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"/>
</FrameLayout>

View File

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
@ -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" />
<ViewStub android:id="@+id/video_player_stub"

View File

@ -14,6 +14,14 @@
android:scaleType="fitCenter"
android:contentDescription="@string/conversation_item__mms_image_description" />
<ImageView
android:id="@+id/thumbnail_caption_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="6dp"
android:src="@drawable/ic_caption"
android:visibility="gone" />
<FrameLayout
android:id="@+id/play_overlay"
android:layout_width="48dp"

View File

@ -12,15 +12,30 @@
app:matProg_linearProgress="true"
app:matProg_spinSpeed="0.333" />
<TextView android:id="@+id/download_details"
android:layout_width="@dimen/transfer_controls_expanded_width"
android:layout_height="@dimen/transfer_controls_contracted_width"
android:padding="15dp"
android:gravity="center"
android:longClickable="false"
android:textColor="?conversation_item_received_text_primary_color"
android:drawableLeft="@drawable/ic_file_download_white_36dp"
android:textSize="16dp"
android:visibility="gone"
android:textStyle="bold" />
<LinearLayout
android:id="@+id/download_details"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_gravity="center"
android:gravity="center"
android:visibility="gone">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_file_download_white_36dp"/>
<TextView android:id="@+id/download_details_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="15dp"
android:gravity="center"
android:longClickable="false"
android:textColor="?conversation_item_received_text_primary_color"
android:textSize="16dp"
android:textStyle="bold" />
</LinearLayout>
</merge>

View File

@ -86,6 +86,7 @@
<attr name="conversation_item_quote_text_color" format="reference"/>
<attr name="conversation_item_sticky_date_background" format="reference" />
<attr name="conversation_item_sticky_date_text_color" format="color" />
<attr name="conversation_item_image_outline_color" format="color" />
<attr name="dialog_info_icon" format="reference" />
<attr name="dialog_alert_icon" format="reference" />
@ -300,4 +301,8 @@
<attr name="typingIndicator_tint" format="color" />
</declare-styleable>
<declare-styleable name="MaxHeightScrollView">
<attr name="scrollView_maxHeight" format="dimension" />
</declare-styleable>
</resources>

View File

@ -10,6 +10,9 @@
<!-- AbstractNotificationBuilder -->
<string name="AbstractNotificationBuilder_new_message">New message</string>
<!-- AlbumThumbnailView -->
<string name="AlbumThumbnailView_plus">\+%d</string>
<!-- ApplicationPreferencesActivity -->
<string name="ApplicationPreferencesActivity_currently_s">Currently: %s</string>
<string name="ApplicationPreferenceActivity_you_havent_set_a_passphrase_yet">You haven\'t set a passphrase yet!</string>
@ -734,6 +737,12 @@
<string name="SingleRecipientNotificationBuilder_signal">Signal</string>
<string name="SingleRecipientNotificationBuilder_new_message">New message</string>
<!-- TransferControlView -->
<plurals name="TransferControlView_n_items">
<item quantity="one">%d Item</item>
<item quantity="other">%d Items</item>
</plurals>
<!-- UnauthorizedReminder -->
<string name="UnauthorizedReminder_device_no_longer_registered">Device no longer registered</string>
<string name="UnauthorizedReminder_this_is_likely_because_you_registered_your_phone_number_with_Signal_on_a_different_device">This is likely because you registered your phone number with Signal on a different device. Tap to re-register.</string>

View File

@ -204,6 +204,7 @@
<item name="conversation_item_quote_text_color">@color/core_grey_90</item>
<item name="conversation_item_sticky_date_background">@drawable/sticky_date_header_background_light</item>
<item name="conversation_item_sticky_date_text_color">@color/core_grey_60</item>
<item name="conversation_item_image_outline_color">@color/transparent_black_30</item>
<item name="quick_camera_icon">@drawable/quick_camera_light</item>
<item name="quick_mic_icon">@drawable/ic_mic_grey600_24dp</item>
@ -311,6 +312,7 @@
<item name="conversation_item_quote_text_color">@color/core_grey_05</item>
<item name="conversation_item_sticky_date_background">@drawable/sticky_date_header_background_dark</item>
<item name="conversation_item_sticky_date_text_color">@color/core_grey_25</item>
<item name="conversation_item_image_outline_color">@color/transparent_white_30</item>
<item name="contact_list_divider">@drawable/contact_list_divider_dark</item>

View File

@ -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<Slide> 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<Slide> 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);

View File

@ -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<Pair<Cursor, Integer>> {
public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity implements RecipientModifiedListener,
LoaderManager.LoaderCallbacks<Pair<Cursor, Integer>>,
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);
}
}

View File

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

View File

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

View File

@ -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;

View File

@ -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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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<Slide> 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<Attachment, Float> 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<Slide> 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<Slide> 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<Slide> 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<Slide> 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<Attachment, Float> 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));
});
}
}

View File

@ -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

View File

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

View File

@ -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;

View File

@ -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(),

View File

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

View File

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

View File

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

View File

@ -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

View File

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

View File

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

View File

@ -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<AlbumRailAdapter.AlbumRailViewHolder> {
private final GlideRequests glideRequests;
private final List<MediaRecord> 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<MediaRecord> 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);
}
}

View File

@ -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> 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<MediaRecord> 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<PreviewData> getPreviewData() {
return previewData;
}
public static class PreviewData {
private final List<MediaRecord> albumThumbnails;
private final String caption;
private final int activePosition;
public PreviewData(@NonNull List<MediaRecord> albumThumbnails, @Nullable String caption, int activePosition) {
this.albumThumbnails = albumThumbnails;
this.caption = caption;
this.activePosition = activePosition;
}
public @NonNull List<MediaRecord> getAlbumThumbnails() {
return albumThumbnails;
}
public @Nullable String getCaption() {
return caption;
}
public int getActivePosition() {
return activePosition;
}
}
}

View File

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

View File

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

View File

@ -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<String> getCaption() {
return Optional.fromNullable(attachment.getCaption());
}
@NonNull
public Optional<String> 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);
}

View File

@ -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<Slide> getThumbnailSlides() {
return Stream.of(slides).filter(Slide::hasImage).toList();
}
public @Nullable AudioSlide getAudioSlide() {
for (Slide slide : slides) {
if (slide.hasAudio()) {

View File

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

View File

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