Support for an audio view to allow in-app playback of audio.

Closes #4270
// FREEBIE
This commit is contained in:
Moxie Marlinspike 2015-10-21 15:32:29 -07:00
parent d2f44f6584
commit 15c6f18750
40 changed files with 1228 additions and 162 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 831 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 786 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 524 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 559 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 959 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

91
res/layout/audio_view.xml Normal file
View File

@ -0,0 +1,91 @@
<?xml version="1.0" encoding="utf-8"?>
<merge 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">
<LinearLayout android:id="@+id/audio_widget_container"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="horizontal">
<org.thoughtcrime.securesms.components.AnimatingToggle
android:id="@+id/control_toggle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center">
<com.pnikosis.materialishprogress.ProgressWheel
android:id="@+id/download_progress"
android:layout_width="48dp"
android:layout_height="48dp"
android:visibility="gone"
android:layout_gravity="center"
app:matProg_barColor="@color/white"
app:matProg_linearProgress="true"
app:matProg_spinSpeed="0.333"
tools:visibility="visible"/>
<ImageView android:id="@+id/play"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:gravity="center_vertical"
android:clickable="true"
android:visibility="gone"
android:background="@drawable/circle_touch_highlight_background"
android:src="@drawable/ic_play_circle_fill_white_48dp"
android:contentDescription="Play"/>
<ImageView android:id="@+id/pause"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:gravity="center_vertical"
android:clickable="true"
android:visibility="gone"
android:background="@drawable/circle_touch_highlight_background"
android:src="@drawable/ic_pause_circle_fill_white_48dp"
android:contentDescription="Pause"/>
<ImageView android:id="@+id/download"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:clickable="true"
android:visibility="gone"
android:background="@drawable/circle_touch_highlight_background"
android:src="@drawable/ic_download_circle_fill_white_48dp"
android:contentDescription="Download"/>
</org.thoughtcrime.securesms.components.AnimatingToggle>
<SeekBar android:id="@+id/seek"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:minWidth="210dp"/>
</LinearLayout>
<TextView android:id="@+id/timestamp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="76dip"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="?conversation_item_sent_text_secondary_color"
android:textSize="@dimen/conversation_item_date_text_size"
android:fontFamily="sans-serif-light"
android:autoLink="none"
android:visibility="gone"
tools:text="00:15"
tools:visibility="visible"
/>
</LinearLayout>
</merge>

View File

@ -37,14 +37,33 @@
android:background="?android:windowBackground" android:background="?android:windowBackground"
android:visibility="gone"> android:visibility="gone">
<org.thoughtcrime.securesms.components.RemovableMediaView
android:id="@+id/removable_media_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center">
<org.thoughtcrime.securesms.components.ThumbnailView <org.thoughtcrime.securesms.components.ThumbnailView
android:id="@+id/attachment_thumbnail" android:id="@+id/attachment_thumbnail"
android:layout_width="230dp" android:layout_width="230dp"
android:layout_height="150dp" android:layout_height="150dp"
android:layout_gravity="center_horizontal" android:layout_gravity="center_horizontal"
android:visibility="gone"
android:contentDescription="@string/conversation_activity__attachment_thumbnail" android:contentDescription="@string/conversation_activity__attachment_thumbnail"
app:backgroundColorHint="?conversation_background" /> app:backgroundColorHint="?conversation_background" />
<org.thoughtcrime.securesms.components.AudioView
android:id="@+id/attachment_audio"
android:layout_width="210dp"
android:layout_height="wrap_content"
android:visibility="gone"
android:background="@color/white"
android:paddingTop="15dp"
android:paddingBottom="15dp"
app:tintColor="@color/grey_500"/>
</org.thoughtcrime.securesms.components.RemovableMediaView>
</FrameLayout> </FrameLayout>
<LinearLayout android:id="@+id/bottom_panel" <LinearLayout android:id="@+id/bottom_panel"

View File

@ -1,12 +1,14 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<org.thoughtcrime.securesms.ConversationItem android:id="@+id/conversation_item" <org.thoughtcrime.securesms.ConversationItem
android:id="@+id/conversation_item"
android:layout_width="fill_parent" android:layout_width="fill_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:paddingRight="10dip" android:paddingRight="10dip"
android:orientation="vertical" android:orientation="vertical"
android:background="@drawable/conversation_item_background" android:background="@drawable/conversation_item_background"
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"> xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto">
<TextView android:id="@+id/group_message_status" <TextView android:id="@+id/group_message_status"
android:layout_width="wrap_content" android:layout_width="wrap_content"
@ -53,7 +55,15 @@
android:contentDescription="@string/conversation_item__mms_image_description" android:contentDescription="@string/conversation_item__mms_image_description"
android:visibility="gone" android:visibility="gone"
tools:src="@drawable/ic_video_light" tools:src="@drawable/ic_video_light"
tools:visibility="visible" /> tools:visibility="gone" />
<org.thoughtcrime.securesms.components.AudioView
android:id="@+id/audio_view"
android:layout_width="210dp"
android:layout_height="wrap_content"
android:visibility="gone"
app:tintColor="@color/white"
tools:visibility="visible"/>
<org.thoughtcrime.securesms.components.emoji.EmojiTextView <org.thoughtcrime.securesms.components.emoji.EmojiTextView
android:id="@+id/conversation_item_body" android:id="@+id/conversation_item_body"

View File

@ -2,6 +2,7 @@
<org.thoughtcrime.securesms.ConversationItem <org.thoughtcrime.securesms.ConversationItem
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/conversation_item" android:id="@+id/conversation_item"
android:layout_width="fill_parent" android:layout_width="fill_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
@ -67,6 +68,13 @@
tools:src="@drawable/ic_video_light" tools:src="@drawable/ic_video_light"
tools:visibility="visible" /> tools:visibility="visible" />
<org.thoughtcrime.securesms.components.AudioView
android:id="@+id/audio_view"
android:layout_width="210dp"
android:layout_height="wrap_content"
app:tintColor="@color/grey_500"
android:visibility="gone"/>
<org.thoughtcrime.securesms.components.emoji.EmojiTextView <org.thoughtcrime.securesms.components.emoji.EmojiTextView
android:id="@+id/conversation_item_body" android:id="@+id/conversation_item_body"
android:autoLink="all" android:autoLink="all"

View File

@ -3,4 +3,6 @@
android:id="@+id/remove_image_button" android:id="@+id/remove_image_button"
android:layout_width="@dimen/media_bubble_remove_button_size" android:layout_width="@dimen/media_bubble_remove_button_size"
android:layout_height="@dimen/media_bubble_remove_button_size" android:layout_height="@dimen/media_bubble_remove_button_size"
android:src="@drawable/conversation_attachment_close_circle" /> android:layout_gravity="top|right"
android:src="@drawable/conversation_attachment_close_circle"
android:visibility="gone"/>

View File

@ -16,9 +16,4 @@
android:layout_gravity="center" android:layout_gravity="center"
android:layout="@layout/transfer_controls_stub" /> android:layout="@layout/transfer_controls_stub" />
<ViewStub android:id="@+id/remove_button_stub"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="top|right"
android:layout="@layout/thumbnail_view_remove_button" />
</merge> </merge>

View File

@ -134,4 +134,9 @@
<declare-styleable name="ThumbnailView"> <declare-styleable name="ThumbnailView">
<attr name="backgroundColorHint" format="color" /> <attr name="backgroundColorHint" format="color" />
</declare-styleable> </declare-styleable>
<declare-styleable name="AudioView">
<attr name="tintColor" format="color" />
</declare-styleable>
</resources> </resources>

View File

@ -4,6 +4,7 @@ import android.support.annotation.NonNull;
import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.database.model.MessageRecord; import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.recipients.Recipients;
import java.util.Locale; import java.util.Locale;
import java.util.Set; import java.util.Set;
@ -13,5 +14,5 @@ public interface BindableConversationItem extends Unbindable {
@NonNull MessageRecord messageRecord, @NonNull MessageRecord messageRecord,
@NonNull Locale locale, @NonNull Locale locale,
@NonNull Set<MessageRecord> batchSelected, @NonNull Set<MessageRecord> batchSelected,
boolean groupThread); @NonNull Recipients recipients);
} }

View File

@ -35,6 +35,7 @@ import org.thoughtcrime.securesms.database.MmsSmsColumns;
import org.thoughtcrime.securesms.database.MmsSmsDatabase; import org.thoughtcrime.securesms.database.MmsSmsDatabase;
import org.thoughtcrime.securesms.database.SmsDatabase; import org.thoughtcrime.securesms.database.SmsDatabase;
import org.thoughtcrime.securesms.database.model.MessageRecord; import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.recipients.Recipients;
import org.thoughtcrime.securesms.util.LRUCache; import org.thoughtcrime.securesms.util.LRUCache;
import java.lang.ref.SoftReference; import java.lang.ref.SoftReference;
@ -71,7 +72,7 @@ public class ConversationAdapter <V extends View & BindableConversationItem>
private final ItemClickListener clickListener; private final ItemClickListener clickListener;
private final MasterSecret masterSecret; private final MasterSecret masterSecret;
private final Locale locale; private final Locale locale;
private final boolean groupThread; private final Recipients recipients;
private final MmsSmsDatabase db; private final MmsSmsDatabase db;
private final LayoutInflater inflater; private final LayoutInflater inflater;
@ -96,13 +97,13 @@ public class ConversationAdapter <V extends View & BindableConversationItem>
@NonNull Locale locale, @NonNull Locale locale,
@Nullable ItemClickListener clickListener, @Nullable ItemClickListener clickListener,
@Nullable Cursor cursor, @Nullable Cursor cursor,
boolean groupThread) @NonNull Recipients recipients)
{ {
super(context, cursor); super(context, cursor);
this.masterSecret = masterSecret; this.masterSecret = masterSecret;
this.locale = locale; this.locale = locale;
this.clickListener = clickListener; this.clickListener = clickListener;
this.groupThread = groupThread; this.recipients = recipients;
this.inflater = LayoutInflater.from(context); this.inflater = LayoutInflater.from(context);
this.db = DatabaseFactory.getMmsSmsDatabase(context); this.db = DatabaseFactory.getMmsSmsDatabase(context);
} }
@ -118,7 +119,7 @@ public class ConversationAdapter <V extends View & BindableConversationItem>
String type = cursor.getString(cursor.getColumnIndexOrThrow(MmsSmsDatabase.TRANSPORT)); String type = cursor.getString(cursor.getColumnIndexOrThrow(MmsSmsDatabase.TRANSPORT));
MessageRecord messageRecord = getMessageRecord(id, cursor, type); MessageRecord messageRecord = getMessageRecord(id, cursor, type);
viewHolder.getView().bind(masterSecret, messageRecord, locale, batchSelected, groupThread); viewHolder.getView().bind(masterSecret, messageRecord, locale, batchSelected, recipients);
} }
@Override public ViewHolder onCreateItemViewHolder(ViewGroup parent, int viewType) { @Override public ViewHolder onCreateItemViewHolder(ViewGroup parent, int viewType) {

View File

@ -142,8 +142,7 @@ public class ConversationFragment extends Fragment
private void initializeListAdapter() { private void initializeListAdapter() {
if (this.recipients != null && this.threadId != -1) { if (this.recipients != null && this.threadId != -1) {
list.setAdapter(new ConversationAdapter(getActivity(), masterSecret, locale, selectionClickListener, null, list.setAdapter(new ConversationAdapter(getActivity(), masterSecret, locale, selectionClickListener, null, this.recipients));
(!this.recipients.isSingleRecipient()) || this.recipients.isGroupRecipient()));
getLoaderManager().restartLoader(0, Bundle.EMPTY, this); getLoaderManager().restartLoader(0, Bundle.EMPTY, this);
} }
} }

View File

@ -41,13 +41,14 @@ import android.widget.Toast;
import com.afollestad.materialdialogs.AlertDialogWrapper; import com.afollestad.materialdialogs.AlertDialogWrapper;
import org.thoughtcrime.securesms.components.AudioView;
import org.thoughtcrime.securesms.components.AvatarImageView; import org.thoughtcrime.securesms.components.AvatarImageView;
import org.thoughtcrime.securesms.components.ThumbnailView; import org.thoughtcrime.securesms.components.ThumbnailView;
import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.database.AttachmentDatabase;
import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.MmsDatabase; import org.thoughtcrime.securesms.database.MmsDatabase;
import org.thoughtcrime.securesms.database.MmsSmsDatabase; import org.thoughtcrime.securesms.database.MmsSmsDatabase;
import org.thoughtcrime.securesms.database.AttachmentDatabase;
import org.thoughtcrime.securesms.database.SmsDatabase; import org.thoughtcrime.securesms.database.SmsDatabase;
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch; import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch;
import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord; import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord;
@ -58,10 +59,13 @@ import org.thoughtcrime.securesms.jobs.MmsSendJob;
import org.thoughtcrime.securesms.jobs.SmsSendJob; import org.thoughtcrime.securesms.jobs.SmsSendJob;
import org.thoughtcrime.securesms.mms.PartAuthority; import org.thoughtcrime.securesms.mms.PartAuthority;
import org.thoughtcrime.securesms.mms.Slide; import org.thoughtcrime.securesms.mms.Slide;
import org.thoughtcrime.securesms.mms.SlideClickListener;
import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.Recipients;
import org.thoughtcrime.securesms.util.DateUtils; import org.thoughtcrime.securesms.util.DateUtils;
import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.util.Util;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Set; import java.util.Set;
@ -75,7 +79,7 @@ import java.util.Set;
*/ */
public class ConversationItem extends LinearLayout public class ConversationItem extends LinearLayout
implements Recipient.RecipientModifiedListener, BindableConversationItem implements Recipient.RecipientModifiedListener, Recipients.RecipientsModifiedListener, BindableConversationItem
{ {
private final static String TAG = ConversationItem.class.getSimpleName(); private final static String TAG = ConversationItem.class.getSimpleName();
@ -98,11 +102,13 @@ public class ConversationItem extends LinearLayout
private View pendingIndicator; private View pendingIndicator;
private ImageView pendingApprovalIndicator; private ImageView pendingApprovalIndicator;
private StatusManager statusManager; private @NonNull Set<MessageRecord> batchSelected = new HashSet<>();
private Set<MessageRecord> batchSelected; private @Nullable Recipients conversationRecipients;
private ThumbnailView mediaThumbnail; private @NonNull StatusManager statusManager;
private Button mmsDownloadButton; private @NonNull ThumbnailView mediaThumbnail;
private TextView mmsDownloadingLabel; private @NonNull AudioView audioView;
private @NonNull Button mmsDownloadButton;
private @NonNull TextView mmsDownloadingLabel;
private int defaultBubbleColor; private int defaultBubbleColor;
@ -152,15 +158,20 @@ public class ConversationItem extends LinearLayout
this.pendingApprovalIndicator = (ImageView) findViewById(R.id.pending_approval_indicator); this.pendingApprovalIndicator = (ImageView) findViewById(R.id.pending_approval_indicator);
this.pendingIndicator = findViewById(R.id.pending_indicator); this.pendingIndicator = findViewById(R.id.pending_indicator);
this.mediaThumbnail = (ThumbnailView) findViewById(R.id.image_view); this.mediaThumbnail = (ThumbnailView) findViewById(R.id.image_view);
this.audioView = (AudioView) findViewById(R.id.audio_view);
this.statusManager = new StatusManager(pendingIndicator, sentIndicator, deliveredIndicator, failedIndicator, pendingApprovalIndicator); this.statusManager = new StatusManager(pendingIndicator, sentIndicator, deliveredIndicator, failedIndicator, pendingApprovalIndicator);
setOnClickListener(new ClickListener(null)); setOnClickListener(new ClickListener(null));
PassthroughClickListener passthroughClickListener = new PassthroughClickListener(); PassthroughClickListener passthroughClickListener = new PassthroughClickListener();
if (mmsDownloadButton != null) mmsDownloadButton.setOnClickListener(mmsDownloadClickListener); AttachmentDownloadClickListener downloadClickListener = new AttachmentDownloadClickListener();
mmsDownloadButton.setOnClickListener(mmsDownloadClickListener);
mediaThumbnail.setThumbnailClickListener(new ThumbnailClickListener()); mediaThumbnail.setThumbnailClickListener(new ThumbnailClickListener());
mediaThumbnail.setDownloadClickListener(new ThumbnailDownloadClickListener()); mediaThumbnail.setDownloadClickListener(downloadClickListener);
mediaThumbnail.setOnLongClickListener(passthroughClickListener); mediaThumbnail.setOnLongClickListener(passthroughClickListener);
mediaThumbnail.setOnClickListener(passthroughClickListener); mediaThumbnail.setOnClickListener(passthroughClickListener);
audioView.setDownloadClickListener(downloadClickListener);
audioView.setOnLongClickListener(passthroughClickListener);
bodyText.setOnLongClickListener(passthroughClickListener); bodyText.setOnLongClickListener(passthroughClickListener);
bodyText.setOnClickListener(passthroughClickListener); bodyText.setOnClickListener(passthroughClickListener);
} }
@ -170,16 +181,18 @@ public class ConversationItem extends LinearLayout
@NonNull MessageRecord messageRecord, @NonNull MessageRecord messageRecord,
@NonNull Locale locale, @NonNull Locale locale,
@NonNull Set<MessageRecord> batchSelected, @NonNull Set<MessageRecord> batchSelected,
boolean groupThread) @NonNull Recipients conversationRecipients)
{ {
this.masterSecret = masterSecret; this.masterSecret = masterSecret;
this.messageRecord = messageRecord; this.messageRecord = messageRecord;
this.locale = locale; this.locale = locale;
this.batchSelected = batchSelected; this.batchSelected = batchSelected;
this.groupThread = groupThread; this.conversationRecipients = conversationRecipients;
this.groupThread = !conversationRecipients.isSingleRecipient() || conversationRecipients.isGroupRecipient();
this.recipient = messageRecord.getIndividualRecipient(); this.recipient = messageRecord.getIndividualRecipient();
this.recipient.addListener(this); this.recipient.addListener(this);
this.conversationRecipients.addListener(this);
setInteractionState(messageRecord); setInteractionState(messageRecord);
setBodyText(messageRecord); setBodyText(messageRecord);
@ -218,6 +231,7 @@ public class ConversationItem extends LinearLayout
if (messageRecord.isOutgoing()) { if (messageRecord.isOutgoing()) {
bodyBubble.getBackground().setColorFilter(defaultBubbleColor, PorterDuff.Mode.MULTIPLY); bodyBubble.getBackground().setColorFilter(defaultBubbleColor, PorterDuff.Mode.MULTIPLY);
mediaThumbnail.setBackgroundColorHint(defaultBubbleColor); mediaThumbnail.setBackgroundColorHint(defaultBubbleColor);
audioView.setTint(conversationRecipients.getColor().toConversationColor(context));
} else { } else {
int color = recipient.getColor().toConversationColor(context); int color = recipient.getColor().toConversationColor(context);
bodyBubble.getBackground().setColorFilter(color, PorterDuff.Mode.MULTIPLY); bodyBubble.getBackground().setColorFilter(color, PorterDuff.Mode.MULTIPLY);
@ -237,7 +251,13 @@ public class ConversationItem extends LinearLayout
return TextUtils.isEmpty(messageRecord.getDisplayBody()) && messageRecord.isMms(); return TextUtils.isEmpty(messageRecord.getDisplayBody()) && messageRecord.isMms();
} }
private boolean hasMedia(MessageRecord messageRecord) { private boolean hasAudio(MessageRecord messageRecord) {
return messageRecord.isMms() &&
!messageRecord.isMmsNotification() &&
((MediaMmsMessageRecord)messageRecord).getSlideDeck().getAudioSlide() != null;
}
private boolean hasThumbnail(MessageRecord messageRecord) {
return messageRecord.isMms() && return messageRecord.isMms() &&
!messageRecord.isMmsNotification() && !messageRecord.isMmsNotification() &&
((MediaMmsMessageRecord)messageRecord).getSlideDeck().getThumbnailSlide() != null; ((MediaMmsMessageRecord)messageRecord).getSlideDeck().getThumbnailSlide() != null;
@ -256,20 +276,33 @@ public class ConversationItem extends LinearLayout
} }
private void setMediaAttributes(MessageRecord messageRecord) { private void setMediaAttributes(MessageRecord messageRecord) {
boolean showControls = !messageRecord.isFailed() && (!messageRecord.isOutgoing() || messageRecord.isPending());
if (messageRecord.isMmsNotification()) { if (messageRecord.isMmsNotification()) {
mediaThumbnail.setVisibility(View.GONE); mediaThumbnail.setVisibility(View.GONE);
audioView.setVisibility(View.GONE);
bodyText.setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)); bodyText.setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT));
setNotificationMmsAttributes((NotificationMmsMessageRecord) messageRecord); setNotificationMmsAttributes((NotificationMmsMessageRecord) messageRecord);
} else if (hasMedia(messageRecord)) { } else if (hasAudio(messageRecord)) {
audioView.setVisibility(View.VISIBLE);
mediaThumbnail.setVisibility(View.GONE);
//noinspection ConstantConditions
audioView.setAudio(masterSecret, ((MediaMmsMessageRecord) messageRecord).getSlideDeck().getAudioSlide(), showControls);
bodyText.setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT));
} else if (hasThumbnail(messageRecord)) {
mediaThumbnail.setVisibility(View.VISIBLE); mediaThumbnail.setVisibility(View.VISIBLE);
audioView.setVisibility(View.GONE);
//noinspection ConstantConditions //noinspection ConstantConditions
mediaThumbnail.setImageResource(masterSecret, mediaThumbnail.setImageResource(masterSecret,
((MediaMmsMessageRecord)messageRecord).getSlideDeck().getThumbnailSlide(), ((MediaMmsMessageRecord)messageRecord).getSlideDeck().getThumbnailSlide(),
!messageRecord.isFailed() && (!messageRecord.isOutgoing() || messageRecord.isPending()), showControls);
false);
bodyText.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); bodyText.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
} else { } else {
mediaThumbnail.setVisibility(View.GONE); mediaThumbnail.setVisibility(View.GONE);
audioView.setVisibility(View.GONE);
bodyText.setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)); bodyText.setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT));
} }
} }
@ -400,7 +433,12 @@ public class ConversationItem extends LinearLayout
}); });
} }
private class ThumbnailDownloadClickListener implements ThumbnailView.ThumbnailClickListener { @Override
public void onModified(Recipients recipient) {
onModified(recipient.getPrimaryRecipient());
}
private class AttachmentDownloadClickListener implements SlideClickListener {
@Override public void onClick(View v, final Slide slide) { @Override public void onClick(View v, final Slide slide) {
DatabaseFactory.getAttachmentDatabase(context).setTransferState(messageRecord.getId(), DatabaseFactory.getAttachmentDatabase(context).setTransferState(messageRecord.getId(),
slide.asAttachment(), slide.asAttachment(),
@ -408,7 +446,7 @@ public class ConversationItem extends LinearLayout
} }
} }
private class ThumbnailClickListener implements ThumbnailView.ThumbnailClickListener { private class ThumbnailClickListener implements SlideClickListener {
private void fireIntent(Slide slide) { private void fireIntent(Slide slide) {
Log.w(TAG, "Clicked: " + slide.getUri() + " , " + slide.getContentType()); Log.w(TAG, "Clicked: " + slide.getUri() + " , " + slide.getContentType());
Intent intent = new Intent(Intent.ACTION_VIEW); Intent intent = new Intent(Intent.ACTION_VIEW);

View File

@ -56,7 +56,7 @@ public class ConversationUpdateItem extends LinearLayout
@NonNull MessageRecord messageRecord, @NonNull MessageRecord messageRecord,
@NonNull Locale locale, @NonNull Locale locale,
@NonNull Set<MessageRecord> batchSelected, @NonNull Set<MessageRecord> batchSelected,
boolean groupThread) @NonNull Recipients conversationRecipients)
{ {
bind(messageRecord, locale); bind(messageRecord, locale);
} }

View File

@ -70,7 +70,7 @@ public class ImageMediaAdapter extends CursorRecyclerViewAdapter<ViewHolder> {
Slide slide = MediaUtil.getSlideForAttachment(getContext(), imageRecord.getAttachment()); Slide slide = MediaUtil.getSlideForAttachment(getContext(), imageRecord.getAttachment());
if (slide != null) { if (slide != null) {
imageView.setImageResource(masterSecret, slide, false, false); imageView.setImageResource(masterSecret, slide, false);
} }
imageView.setOnClickListener(new OnMediaClickListener(imageRecord)); imageView.setOnClickListener(new OnMediaClickListener(imageRecord));

View File

@ -172,8 +172,7 @@ public class MessageDetailsActivity extends PassphraseRequiredActionBarActivity
} }
toFrom.setText(toFromRes); toFrom.setText(toFromRes);
conversationItem.bind(masterSecret, messageRecord, dynamicLanguage.getCurrentLocale(), conversationItem.bind(masterSecret, messageRecord, dynamicLanguage.getCurrentLocale(),
new HashSet<MessageRecord>(), new HashSet<MessageRecord>(), recipients);
recipients != messageRecord.getRecipients());
recipientsList.setAdapter(new MessageDetailsRecipientAdapter(this, masterSecret, messageRecord, recipientsList.setAdapter(new MessageDetailsRecipientAdapter(this, masterSecret, messageRecord,
recipients, isPushGroup)); recipients, isPushGroup));
} }

View File

@ -13,15 +13,15 @@ import java.io.InputStream;
public class UriAttachment extends Attachment { public class UriAttachment extends Attachment {
private final Uri dataUri; private final @NonNull Uri dataUri;
private final Uri thumbnailUri; private final @NonNull Uri thumbnailUri;
public UriAttachment(Uri uri, String contentType, int transferState, long size) { public UriAttachment(@NonNull Uri uri, @NonNull String contentType, int transferState, long size) {
this(uri, uri, contentType, transferState, size); this(uri, uri, contentType, transferState, size);
} }
public UriAttachment(Uri dataUri, Uri thumbnailUri, public UriAttachment(@NonNull Uri dataUri, @NonNull Uri thumbnailUri,
String contentType, int transferState, long size) @NonNull String contentType, int transferState, long size)
{ {
super(contentType, transferState, size, null, null, null); super(contentType, transferState, size, null, null, null);
this.dataUri = dataUri; this.dataUri = dataUri;
@ -39,4 +39,14 @@ public class UriAttachment extends Attachment {
public Uri getThumbnailUri() { public Uri getThumbnailUri() {
return thumbnailUri; return thumbnailUri;
} }
@Override
public boolean equals(Object other) {
return other != null && other instanceof UriAttachment && ((UriAttachment) other).dataUri.equals(this.dataUri);
}
@Override
public int hashCode() {
return dataUri.hashCode();
}
} }

View File

@ -0,0 +1,376 @@
package org.thoughtcrime.securesms.audio;
import android.content.Context;
import android.net.Uri;
import android.support.annotation.NonNull;
import android.util.Log;
import org.spongycastle.util.encoders.Hex;
import org.thoughtcrime.securesms.attachments.Attachment;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.mms.PartAuthority;
import org.thoughtcrime.securesms.util.Util;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.net.UnknownHostException;
import java.security.MessageDigest;
import java.util.Map;
import java.util.Properties;
import java.util.StringTokenizer;
/**
* @author Stefan "frostymarvelous" Froelich <stefan d0t froelich At whisppa DoT com>
*/
public class AudioAttachmentServer implements Runnable {
private static final String TAG = AudioAttachmentServer.class.getSimpleName();
private final Context context;
private final MasterSecret masterSecret;
private final Attachment attachment;
private final ServerSocket socket;
private final int port;
private final String auth;
private volatile boolean isRunning;
public AudioAttachmentServer(Context context, MasterSecret masterSecret, Attachment attachment)
throws IOException
{
try {
this.context = context;
this.masterSecret = masterSecret;
this.attachment = attachment;
this.socket = new ServerSocket(0, 0, InetAddress.getByAddress(new byte[]{127, 0, 0, 1}));
this.port = socket.getLocalPort();
this.auth = new String(Hex.encode(Util.getSecretBytes(16)));
this.socket.setSoTimeout(5000);
} catch (UnknownHostException e) {
throw new AssertionError(e);
}
}
public Uri getUri() {
return Uri.parse(String.format("http://127.0.0.1:%d/%s", port, auth));
}
public void start() {
isRunning = true;
new Thread(this).start();
}
public void stop() {
isRunning = false;
}
@Override
public void run() {
while (isRunning) {
Socket client = null;
try {
client = socket.accept();
if (client != null) {
StreamToMediaPlayerTask task = new StreamToMediaPlayerTask(client, "/" + auth);
if (task.processRequest()) {
task.execute();
}
}
} catch (SocketTimeoutException e) {
Log.w(TAG, e);
} catch (IOException e) {
Log.e(TAG, "Error connecting to client", e);
} finally {
try {if (client != null) client.close();} catch (IOException e) {}
}
}
Log.d(TAG, "Proxy interrupted. Shutting down.");
}
private class StreamToMediaPlayerTask {
private final @NonNull Socket client;
private final @NonNull String auth;
private long cbSkip;
private Properties parameters;
private Properties request;
private Properties requestHeaders;
// private String filePath;
public StreamToMediaPlayerTask(@NonNull Socket client, @NonNull String auth) {
this.client = client;
this.auth = auth;
}
public boolean processRequest() throws IOException {
InputStream is = client.getInputStream();
final int bufferSize = 8192;
byte[] buffer = new byte[bufferSize];
int splitByte = 0;
int readLength = 0;
{
int read = is.read(buffer, 0, bufferSize);
while (read > 0) {
readLength += read;
splitByte = findHeaderEnd(buffer, readLength);
if (splitByte > 0)
break;
read = is.read(buffer, readLength, bufferSize - readLength);
}
}
// Create a BufferedReader for parsing the header.
ByteArrayInputStream hbis = new ByteArrayInputStream(buffer, 0, readLength);
BufferedReader hin = new BufferedReader(new InputStreamReader(hbis));
request = new Properties();
parameters = new Properties();
requestHeaders = new Properties();
try {
decodeHeader(hin, request, parameters, requestHeaders);
} catch (InterruptedException e1) {
Log.e(TAG, "Exception: " + e1.getMessage());
e1.printStackTrace();
}
for (Map.Entry<Object, Object> e : requestHeaders.entrySet()) {
Log.i(TAG, "Header: " + e.getKey() + " : " + e.getValue());
}
String range = requestHeaders.getProperty("range");
if (range != null) {
Log.i(TAG, "range is: " + range);
range = range.substring(6);
int charPos = range.indexOf('-');
if (charPos > 0) {
range = range.substring(0, charPos);
}
cbSkip = Long.parseLong(range);
Log.i(TAG, "range found!! " + cbSkip);
}
if(!request.get("method").equals("GET")) {
Log.e(TAG, "Only GET is supported");
return false;
}
String receivedAuth = request.getProperty("uri");
if (receivedAuth == null || !MessageDigest.isEqual(receivedAuth.getBytes(), auth.getBytes())) {
Log.w(TAG, "Bad auth token!");
return false;
}
// filePath = request.getProperty("uri");
return true;
}
protected void execute() throws IOException {
InputStream inputStream = PartAuthority.getAttachmentStream(context, masterSecret, attachment.getDataUri());
long fileSize = attachment.getSize();
String headers = "";
if (cbSkip > 0) {// It is a seek or skip request if there's a Range
// header
headers += "HTTP/1.1 206 Partial Content\r\n";
headers += "Content-Type: " + attachment.getContentType() + "\r\n";
headers += "Accept-Ranges: bytes\r\n";
headers += "Content-Length: " + (fileSize - cbSkip) + "\r\n";
headers += "Content-Range: bytes " + cbSkip + "-" + (fileSize - 1) + "/" + fileSize + "\r\n";
headers += "Connection: Keep-Alive\r\n";
headers += "\r\n";
} else {
headers += "HTTP/1.1 200 OK\r\n";
headers += "Content-Type: " + attachment.getContentType() + "\r\n";
headers += "Accept-Ranges: bytes\r\n";
headers += "Content-Length: " + fileSize + "\r\n";
headers += "Connection: Keep-Alive\r\n";
headers += "\r\n";
}
Log.i(TAG, "headers: " + headers);
OutputStream output = null;
byte[] buff = new byte[64 * 1024];
try {
output = new BufferedOutputStream(client.getOutputStream(), 32 * 1024);
output.write(headers.getBytes());
inputStream.skip(cbSkip);
// dataSource.skipFully(data, cbSkip);//try to skip as much as possible
// Loop as long as there's stuff to send and client has not closed
int cbRead;
while (!client.isClosed() && (cbRead = inputStream.read(buff, 0, buff.length)) != -1) {
output.write(buff, 0, cbRead);
}
}
catch (SocketException socketException) {
Log.e(TAG, "SocketException() thrown, proxy client has probably closed. This can exit harmlessly");
}
catch (Exception e) {
Log.e(TAG, "Exception thrown from streaming task:");
Log.e(TAG, e.getClass().getName() + " : " + e.getLocalizedMessage());
}
// Cleanup
try {
if (output != null) {
output.close();
}
client.close();
}
catch (IOException e) {
Log.e(TAG, "IOException while cleaning up streaming task:");
Log.e(TAG, e.getClass().getName() + " : " + e.getLocalizedMessage());
e.printStackTrace();
}
}
/**
* Find byte index separating header from body. It must be the last byte of
* the first two sequential new lines.
**/
private int findHeaderEnd(final byte[] buf, int rlen) {
int splitbyte = 0;
while (splitbyte + 3 < rlen) {
if (buf[splitbyte] == '\r' && buf[splitbyte + 1] == '\n'
&& buf[splitbyte + 2] == '\r' && buf[splitbyte + 3] == '\n')
return splitbyte + 4;
splitbyte++;
}
return 0;
}
/**
* Decodes the sent headers and loads the data into java Properties' key -
* value pairs
**/
private void decodeHeader(BufferedReader in, Properties pre,
Properties parms, Properties header) throws InterruptedException {
try {
// Read the request line
String inLine = in.readLine();
if (inLine == null)
return;
StringTokenizer st = new StringTokenizer(inLine);
if (!st.hasMoreTokens())
Log.e(TAG,
"BAD REQUEST: Syntax error. Usage: GET /example/file.html");
String method = st.nextToken();
pre.put("method", method);
if (!st.hasMoreTokens())
Log.e(TAG,
"BAD REQUEST: Missing URI. Usage: GET /example/file.html");
String uri = st.nextToken();
// Decode parameters from the URI
int qmi = uri.indexOf('?');
if (qmi >= 0) {
decodeParms(uri.substring(qmi + 1), parms);
uri = decodePercent(uri.substring(0, qmi));
} else
uri = decodePercent(uri);
// If there's another token, it's protocol version,
// followed by HTTP headers. Ignore version but parse headers.
// NOTE: this now forces header names lowercase since they are
// case insensitive and vary by client.
if (st.hasMoreTokens()) {
String line = in.readLine();
while (line != null && line.trim().length() > 0) {
int p = line.indexOf(':');
if (p >= 0)
header.put(line.substring(0, p).trim().toLowerCase(),
line.substring(p + 1).trim());
line = in.readLine();
}
}
pre.put("uri", uri);
} catch (IOException ioe) {
Log.e(TAG,
"SERVER INTERNAL ERROR: IOException: " + ioe.getMessage());
}
}
/**
* Decodes parameters in percent-encoded URI-format ( e.g.
* "name=Jack%20Daniels&pass=Single%20Malt" ) and adds them to given
* Properties. NOTE: this doesn't support multiple identical keys due to the
* simplicity of Properties -- if you need multiples, you might want to
* replace the Properties with a Hashtable of Vectors or such.
*/
private void decodeParms(String parms, Properties p)
throws InterruptedException {
if (parms == null)
return;
StringTokenizer st = new StringTokenizer(parms, "&");
while (st.hasMoreTokens()) {
String e = st.nextToken();
int sep = e.indexOf('=');
if (sep >= 0)
p.put(decodePercent(e.substring(0, sep)).trim(),
decodePercent(e.substring(sep + 1)));
}
}
/**
* Decodes the percent encoding scheme. <br/>
* For example: "an+example%20string" -> "an example string"
*/
private String decodePercent(String str) throws InterruptedException {
try {
StringBuffer sb = new StringBuffer();
for (int i = 0; i < str.length(); i++) {
char c = str.charAt(i);
switch (c) {
case '+':
sb.append(' ');
break;
case '%':
sb.append((char) Integer.parseInt(
str.substring(i + 1, i + 3), 16));
i += 2;
break;
default:
sb.append(c);
break;
}
}
return sb.toString();
} catch (Exception e) {
Log.e(TAG, "BAD REQUEST: Bad percent-encoding.");
return null;
}
}
}
}

View File

@ -0,0 +1,237 @@
package org.thoughtcrime.securesms.audio;
import android.content.Context;
import android.media.AudioManager;
import android.media.MediaPlayer;
import android.os.Handler;
import android.os.Message;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.Log;
import android.util.Pair;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.mms.AudioSlide;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.libaxolotl.util.guava.Optional;
import java.io.IOException;
import java.lang.ref.WeakReference;
public class AudioSlidePlayer {
private static final String TAG = AudioSlidePlayer.class.getSimpleName();
private static @NonNull Optional<AudioSlidePlayer> playing = Optional.absent();
private final @NonNull Context context;
private final @NonNull MasterSecret masterSecret;
private final @NonNull AudioSlide slide;
private final @NonNull Handler progressEventHandler;
private @NonNull WeakReference<Listener> listener;
private @Nullable MediaPlayer mediaPlayer;
private @Nullable AudioAttachmentServer audioAttachmentServer;
public synchronized static AudioSlidePlayer createFor(@NonNull Context context,
@NonNull MasterSecret masterSecret,
@NonNull AudioSlide slide,
@NonNull Listener listener)
{
if (playing.isPresent() && playing.get().getAudioSlide().equals(slide)) {
playing.get().setListener(listener);
return playing.get();
} else {
return new AudioSlidePlayer(context, masterSecret, slide, listener);
}
}
private AudioSlidePlayer(@NonNull Context context,
@NonNull MasterSecret masterSecret,
@NonNull AudioSlide slide,
@NonNull Listener listener)
{
this.context = context;
this.masterSecret = masterSecret;
this.slide = slide;
this.listener = new WeakReference<>(listener);
this.progressEventHandler = new ProgressEventHandler(this);
}
public void play(final double progress) throws IOException {
if (this.mediaPlayer != null) return;
this.mediaPlayer = new MediaPlayer();
this.audioAttachmentServer = new AudioAttachmentServer(context, masterSecret, slide.asAttachment());
audioAttachmentServer.start();
mediaPlayer.setDataSource(context, audioAttachmentServer.getUri());
mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
mediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
@Override
public void onPrepared(MediaPlayer mp) {
Log.w(TAG, "onPrepared");
if (progress > 0) {
mediaPlayer.seekTo((int)(mediaPlayer.getDuration() * progress));
}
mediaPlayer.start();
notifyOnStart();
setPlaying(AudioSlidePlayer.this);
progressEventHandler.sendEmptyMessage(0);
}
});
mediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
@Override
public void onCompletion(MediaPlayer mp) {
Log.w(TAG, "onComplete");
mediaPlayer = null;
audioAttachmentServer.stop();
audioAttachmentServer = null;
notifyOnStop();
progressEventHandler.removeMessages(0);
}
});
mediaPlayer.setOnErrorListener(new MediaPlayer.OnErrorListener() {
@Override
public boolean onError(MediaPlayer mp, int what, int extra) {
Log.w(TAG, "MediaPlayer Error: " + what + " , " + extra);
notifyOnStop();
return true;
}
});
mediaPlayer.prepareAsync();
}
public void stop() {
Log.w(TAG, "Stop called!");
shutdown();
}
public void setListener(@NonNull Listener listener) {
this.listener = new WeakReference<>(listener);
if (this.mediaPlayer != null && this.mediaPlayer.isPlaying()) {
notifyOnStart();
}
}
public @NonNull AudioSlide getAudioSlide() {
return slide;
}
private void shutdown() {
removePlaying(this);
if (this.mediaPlayer != null) {
this.mediaPlayer.stop();
}
if (this.audioAttachmentServer != null) {
this.audioAttachmentServer.stop();
}
this.mediaPlayer = null;
this.audioAttachmentServer = null;
}
private Pair<Double, Integer> getProgress() {
if (mediaPlayer == null || mediaPlayer.getCurrentPosition() <= 0 || mediaPlayer.getDuration() <= 0) {
return new Pair<>(0D, 0);
} else {
return new Pair<>((double) mediaPlayer.getCurrentPosition() / (double) mediaPlayer.getDuration(),
mediaPlayer.getCurrentPosition());
}
}
private void notifyOnStart() {
Util.runOnMain(new Runnable() {
@Override
public void run() {
getListener().onStart();
}
});
}
private void notifyOnStop() {
Util.runOnMain(new Runnable() {
@Override
public void run() {
getListener().onStop();
}
});
}
private void notifyOnProgress(final double progress, final long millis) {
Util.runOnMain(new Runnable() {
@Override
public void run() {
getListener().onProgress(progress, millis);
}
});
}
private @NonNull Listener getListener() {
Listener listener = this.listener.get();
if (listener != null) return listener;
else return new Listener() {
@Override
public void onStart() {}
@Override
public void onStop() {}
@Override
public void onProgress(double progress, long millis) {}
};
}
private synchronized static void setPlaying(@NonNull AudioSlidePlayer player) {
if (playing.isPresent() && playing.get() != player) {
playing.get().notifyOnStop();
playing.get().stop();
}
playing = Optional.of(player);
}
private synchronized static void removePlaying(@NonNull AudioSlidePlayer player) {
if (playing.isPresent() && playing.get() == player) {
playing = Optional.absent();
}
}
public interface Listener {
public void onStart();
public void onStop();
public void onProgress(double progress, long millis);
}
private static class ProgressEventHandler extends Handler {
private final WeakReference<AudioSlidePlayer> playerReference;
private ProgressEventHandler(@NonNull AudioSlidePlayer player) {
this.playerReference = new WeakReference<>(player);
}
@Override
public void handleMessage(Message msg) {
AudioSlidePlayer player = playerReference.get();
if (player == null || player.mediaPlayer == null || !player.mediaPlayer.isPlaying()) {
return;
}
Pair<Double, Integer> progress = player.getProgress();
player.notifyOnProgress(progress.first, progress.second);
sendEmptyMessageDelayed(0, 50);
}
}
}

View File

@ -57,4 +57,12 @@ public class AnimatingToggle extends FrameLayout {
current = view; current = view;
} }
public void displayQuick(@Nullable View view) {
if (view == current) return;
if (current != null) current.setVisibility(View.GONE);
if (view != null) view.setVisibility(View.VISIBLE);
current = view;
}
} }

View File

@ -0,0 +1,233 @@
package org.thoughtcrime.securesms.components;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Color;
import android.graphics.PorterDuff;
import android.os.Build;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.SeekBar;
import android.widget.TextView;
import com.pnikosis.materialishprogress.ProgressWheel;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.audio.AudioSlidePlayer;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.database.AttachmentDatabase;
import org.thoughtcrime.securesms.jobs.PartProgressEvent;
import org.thoughtcrime.securesms.mms.AudioSlide;
import org.thoughtcrime.securesms.mms.SlideClickListener;
import org.thoughtcrime.securesms.util.Util;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
public class AudioView extends FrameLayout implements AudioSlidePlayer.Listener {
private static final String TAG = AudioView.class.getSimpleName();
private final @NonNull AnimatingToggle controlToggle;
private final @NonNull ImageView playButton;
private final @NonNull ImageView pauseButton;
private final @NonNull ImageView downloadButton;
private final @NonNull ProgressWheel downloadProgress;
private final @NonNull SeekBar seekBar;
private final @NonNull TextView timestamp;
private @Nullable SlideClickListener downloadListener;
private @Nullable AudioSlidePlayer audioSlidePlayer;
private int backwardsCounter;
public AudioView(Context context) {
this(context, null);
}
public AudioView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public AudioView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
inflate(context, R.layout.audio_view, this);
this.controlToggle = (AnimatingToggle) findViewById(R.id.control_toggle);
this.playButton = (ImageView) findViewById(R.id.play);
this.pauseButton = (ImageView) findViewById(R.id.pause);
this.downloadButton = (ImageView) findViewById(R.id.download);
this.downloadProgress = (ProgressWheel) findViewById(R.id.download_progress);
this.seekBar = (SeekBar) findViewById(R.id.seek);
this.timestamp = (TextView) findViewById(R.id.timestamp);
this.playButton.setOnClickListener(new PlayClickedListener());
this.pauseButton.setOnClickListener(new PauseClickedListener());
this.seekBar.setOnSeekBarChangeListener(new SeekBarModifiedListener());
if (attrs != null) {
TypedArray typedArray = context.getTheme().obtainStyledAttributes(attrs, R.styleable.AudioView, 0, 0);
setTint(typedArray.getColor(R.styleable.AudioView_tintColor, Color.WHITE));
typedArray.recycle();
}
}
public void setAudio(final @NonNull MasterSecret masterSecret,
final @NonNull AudioSlide audio,
final boolean showControls)
{
if (showControls && audio.isPendingDownload()) {
controlToggle.displayQuick(downloadButton);
seekBar.setEnabled(false);
downloadButton.setOnClickListener(new DownloadClickedListener(audio));
if (downloadProgress.isSpinning()) downloadProgress.stopSpinning();
} else if (showControls && audio.getTransferState() == AttachmentDatabase.TRANSFER_PROGRESS_STARTED) {
controlToggle.displayQuick(downloadProgress);
seekBar.setEnabled(false);
downloadProgress.spin();
} else {
controlToggle.displayQuick(playButton);
seekBar.setEnabled(true);
if (downloadProgress.isSpinning()) downloadProgress.stopSpinning();
}
this.audioSlidePlayer = AudioSlidePlayer.createFor(getContext(), masterSecret, audio, this);
}
public void cleanup() {
if (this.audioSlidePlayer != null && pauseButton.getVisibility() == View.VISIBLE) {
this.audioSlidePlayer.stop();
}
}
public void setDownloadClickListener(@Nullable SlideClickListener listener) {
this.downloadListener = listener;
}
@Override
public void onStart() {
this.controlToggle.display(this.pauseButton);
}
@Override
public void onStop() {
this.controlToggle.display(this.playButton);
}
@Override
public void onProgress(double progress, long millis) {
int seekProgress = (int)Math.floor(progress * this.seekBar.getMax());
if (seekProgress > seekBar.getProgress() || backwardsCounter > 3) {
backwardsCounter = 0;
this.seekBar.setProgress(seekProgress);
this.timestamp.setText(String.format("%02d:%02d",
TimeUnit.MILLISECONDS.toMinutes(millis),
TimeUnit.MILLISECONDS.toSeconds(millis)));
} else {
backwardsCounter++;
}
}
public void setTint(int tint) {
this.playButton.setColorFilter(tint, PorterDuff.Mode.SRC_IN);
this.pauseButton.setColorFilter(tint, PorterDuff.Mode.SRC_IN);
this.downloadButton.setColorFilter(tint, PorterDuff.Mode.SRC_IN);
this.downloadProgress.setBarColor(tint);
this.timestamp.setTextColor(tint);
this.seekBar.getProgressDrawable().setColorFilter(tint, PorterDuff.Mode.SRC_IN);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
this.seekBar.getThumb().setColorFilter(tint, PorterDuff.Mode.SRC_IN);
}
}
private double getProgress() {
if (this.seekBar.getProgress() <= 0 || this.seekBar.getMax() <= 0) {
return 0;
} else {
return (double)this.seekBar.getProgress() / (double)this.seekBar.getMax();
}
}
private class PlayClickedListener implements View.OnClickListener {
@Override
public void onClick(View v) {
try {
Log.w(TAG, "playbutton onClick");
if (audioSlidePlayer != null) {
controlToggle.display(pauseButton);
audioSlidePlayer.play(getProgress());
}
} catch (IOException e) {
Log.w(TAG, e);
}
}
}
private class PauseClickedListener implements View.OnClickListener {
@Override
public void onClick(View v) {
Log.w(TAG, "pausebutton onClick");
if (audioSlidePlayer != null) {
controlToggle.display(playButton);
audioSlidePlayer.stop();
}
}
}
private class DownloadClickedListener implements View.OnClickListener {
private final @NonNull AudioSlide slide;
private DownloadClickedListener(@NonNull AudioSlide slide) {
this.slide = slide;
}
@Override
public void onClick(View v) {
if (downloadListener != null) downloadListener.onClick(v, slide);
}
}
private class SeekBarModifiedListener implements SeekBar.OnSeekBarChangeListener {
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {}
@Override
public synchronized void onStartTrackingTouch(SeekBar seekBar) {
if (audioSlidePlayer != null && pauseButton.getVisibility() == View.VISIBLE) {
audioSlidePlayer.stop();
}
}
@Override
public synchronized void onStopTrackingTouch(SeekBar seekBar) {
try {
if (audioSlidePlayer != null && pauseButton.getVisibility() == View.VISIBLE) {
audioSlidePlayer.play(getProgress());
}
} catch (IOException e) {
Log.w(TAG, e);
}
}
}
@SuppressWarnings("unused")
public void onEventAsync(final PartProgressEvent event) {
if (audioSlidePlayer != null && event.attachment.equals(this.audioSlidePlayer.getAudioSlide().asAttachment())) {
Util.runOnMain(new Runnable() {
@Override
public void run() {
downloadProgress.setInstantProgress(((float) event.progress) / event.total);
}
});
}
}
}

View File

@ -0,0 +1,65 @@
package org.thoughtcrime.securesms.components;
import android.content.Context;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.FrameLayout;
import android.widget.ImageView;
import org.thoughtcrime.securesms.R;
public class RemovableMediaView extends FrameLayout {
private final @NonNull ImageView remove;
private final int removeSize;
private @Nullable View current;
public RemovableMediaView(Context context) {
this(context, null);
}
public RemovableMediaView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public RemovableMediaView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
this.remove = (ImageView)LayoutInflater.from(context).inflate(R.layout.media_view_remove_button, this, false);
this.removeSize = getResources().getDimensionPixelSize(R.dimen.media_bubble_remove_button_size);
this.remove.setVisibility(View.GONE);
}
@Override
public void onFinishInflate() {
super.onFinishInflate();
this.addView(remove);
}
public void display(@Nullable View view) {
if (view == current) return;
if (current != null) current.setVisibility(View.GONE);
if (view != null) {
MarginLayoutParams params = (MarginLayoutParams)view.getLayoutParams();
params.setMargins(0, removeSize / 2, removeSize / 2, 0);
view.setLayoutParams(params);
view.setVisibility(View.VISIBLE);
remove.setVisibility(View.VISIBLE);
} else {
remove.setVisibility(View.GONE);
}
current = view;
}
public void setRemoveClickListener(View.OnClickListener listener) {
this.remove.setOnClickListener(listener);
}
}

View File

@ -17,34 +17,30 @@ import android.widget.ImageView;
import com.bumptech.glide.DrawableRequestBuilder; import com.bumptech.glide.DrawableRequestBuilder;
import com.bumptech.glide.GenericRequestBuilder; import com.bumptech.glide.GenericRequestBuilder;
import com.bumptech.glide.Glide; import com.bumptech.glide.Glide;
import com.bumptech.glide.load.resource.bitmap.GlideBitmapDrawable;
import com.bumptech.glide.load.resource.drawable.GlideDrawable;
import com.bumptech.glide.request.RequestListener;
import com.bumptech.glide.request.target.Target;
import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.attachments.Attachment;
import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.database.AttachmentDatabase; import org.thoughtcrime.securesms.database.AttachmentDatabase;
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri; import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri;
import org.thoughtcrime.securesms.mms.RoundedCorners; import org.thoughtcrime.securesms.mms.RoundedCorners;
import org.thoughtcrime.securesms.mms.Slide; import org.thoughtcrime.securesms.mms.Slide;
import org.thoughtcrime.securesms.mms.SlideClickListener;
import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.ViewUtil; import org.thoughtcrime.securesms.util.ViewUtil;
import org.whispersystems.libaxolotl.util.guava.Optional; import org.whispersystems.libaxolotl.util.guava.Optional;
public class ThumbnailView extends FrameLayout { public class ThumbnailView extends FrameLayout {
private static final String TAG = ThumbnailView.class.getSimpleName(); private static final String TAG = ThumbnailView.class.getSimpleName();
private ImageView image; private ImageView image;
private ImageView removeButton;
private int backgroundColorHint; private int backgroundColorHint;
private int radius; private int radius;
private OnClickListener parentClickListener; private OnClickListener parentClickListener;
private Optional<TransferControlView> transferControls = Optional.absent(); private Optional<TransferControlView> transferControls = Optional.absent();
private ThumbnailClickListener thumbnailClickListener = null; private SlideClickListener thumbnailClickListener = null;
private ThumbnailClickListener downloadClickListener = null; private SlideClickListener downloadClickListener = null;
private Slide slide = null; private Slide slide = null;
public ThumbnailView(Context context) { public ThumbnailView(Context context) {
@ -57,9 +53,11 @@ public class ThumbnailView extends FrameLayout {
public ThumbnailView(final Context context, AttributeSet attrs, int defStyle) { public ThumbnailView(final Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle); super(context, attrs, defStyle);
inflate(context, R.layout.thumbnail_view, this); inflate(context, R.layout.thumbnail_view, this);
radius = getResources().getDimensionPixelSize(R.dimen.message_bubble_corner_radius);
image = (ImageView) findViewById(R.id.thumbnail_image); this.radius = getResources().getDimensionPixelSize(R.dimen.message_bubble_corner_radius);
this.image = (ImageView) findViewById(R.id.thumbnail_image);
super.setOnClickListener(new ThumbnailClickDispatcher()); super.setOnClickListener(new ThumbnailClickDispatcher());
if (attrs != null) { if (attrs != null) {
@ -86,21 +84,6 @@ public class ThumbnailView extends FrameLayout {
if (transferControls.isPresent()) transferControls.get().setClickable(clickable); if (transferControls.isPresent()) transferControls.get().setClickable(clickable);
} }
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
if (removeButton != null) {
final int paddingHorizontal = removeButton.getWidth() / 2;
final int paddingVertical = removeButton.getHeight() / 2;
image.setPadding(paddingHorizontal, paddingVertical, paddingHorizontal, 0);
}
}
private ImageView getRemoveButton() {
if (removeButton == null) removeButton = ViewUtil.inflateStub(this, R.id.remove_button_stub);
return removeButton;
}
private TransferControlView getTransferControls() { private TransferControlView getTransferControls() {
if (!transferControls.isPresent()) { if (!transferControls.isPresent()) {
transferControls = Optional.of((TransferControlView)ViewUtil.inflateStub(this, R.id.transfer_controls_stub)); transferControls = Optional.of((TransferControlView)ViewUtil.inflateStub(this, R.id.transfer_controls_stub));
@ -112,9 +95,8 @@ public class ThumbnailView extends FrameLayout {
this.backgroundColorHint = color; this.backgroundColorHint = color;
} }
public void setImageResource(@NonNull MasterSecret masterSecret, @NonNull Slide slide, public void setImageResource(@NonNull MasterSecret masterSecret, @NonNull Slide slide, boolean showControls) {
boolean showControls, boolean showRemove)
{
if (Util.equals(slide, this.slide)) { if (Util.equals(slide, this.slide)) {
Log.w(TAG, "Not re-loading slide " + slide.asAttachment().getDataUri()); Log.w(TAG, "Not re-loading slide " + slide.asAttachment().getDataUri());
return; return;
@ -137,22 +119,16 @@ public class ThumbnailView extends FrameLayout {
this.slide = slide; this.slide = slide;
if (slide.getThumbnailUri() != null) buildThumbnailGlideRequest(slide, masterSecret, showRemove).into(image); if (slide.getThumbnailUri() != null) buildThumbnailGlideRequest(slide, masterSecret).into(image);
else if (slide.hasPlaceholder()) buildPlaceholderGlideRequest(slide).into(image); else if (slide.hasPlaceholder()) buildPlaceholderGlideRequest(slide).into(image);
else Glide.clear(image); else Glide.clear(image);
} }
public void setThumbnailClickListener(ThumbnailClickListener listener) { public void setThumbnailClickListener(SlideClickListener listener) {
this.thumbnailClickListener = listener; this.thumbnailClickListener = listener;
} }
public void setRemoveClickListener(OnClickListener listener) { public void setDownloadClickListener(SlideClickListener listener) {
getRemoveButton().setOnClickListener(listener);
final int pad = getResources().getDimensionPixelSize(R.dimen.media_bubble_remove_button_size);
image.setPadding(pad, pad, pad, 0);
}
public void setDownloadClickListener(ThumbnailClickListener listener) {
this.downloadClickListener = listener; this.downloadClickListener = listener;
} }
@ -174,15 +150,11 @@ public class ThumbnailView extends FrameLayout {
!((Activity)getContext()).isDestroyed(); !((Activity)getContext()).isDestroyed();
} }
private GenericRequestBuilder buildThumbnailGlideRequest(@NonNull Slide slide, @NonNull MasterSecret masterSecret, boolean showRemove) { private GenericRequestBuilder buildThumbnailGlideRequest(@NonNull Slide slide, @NonNull MasterSecret masterSecret) {
DrawableRequestBuilder<DecryptableUri> builder = Glide.with(getContext()).load(new DecryptableUri(masterSecret, slide.getThumbnailUri())) DrawableRequestBuilder<DecryptableUri> builder = Glide.with(getContext()).load(new DecryptableUri(masterSecret, slide.getThumbnailUri()))
.crossFade() .crossFade()
.transform(new RoundedCorners(getContext(), true, radius, backgroundColorHint)); .transform(new RoundedCorners(getContext(), true, radius, backgroundColorHint));
if (showRemove) {
builder = builder.listener(new ThumbnailSetListener(slide.asAttachment()));
}
if (slide.isInProgress()) return builder; if (slide.isInProgress()) return builder;
else return builder.error(R.drawable.ic_missing_thumbnail_picture); else return builder.error(R.drawable.ic_missing_thumbnail_picture);
} }
@ -193,10 +165,6 @@ public class ThumbnailView extends FrameLayout {
.fitCenter(); .fitCenter();
} }
public interface ThumbnailClickListener {
void onClick(View v, Slide slide);
}
private class ThumbnailClickDispatcher implements View.OnClickListener { private class ThumbnailClickDispatcher implements View.OnClickListener {
@Override @Override
public void onClick(View view) { public void onClick(View view) {
@ -220,36 +188,4 @@ public class ThumbnailView extends FrameLayout {
} }
} }
} }
private class ThumbnailSetListener implements RequestListener<Object, GlideDrawable> {
private final Attachment attachment;
public ThumbnailSetListener(@NonNull Attachment attachment) {
this.attachment = attachment;
}
@Override
public boolean onException(Exception e, Object model, Target<GlideDrawable> target, boolean isFirstResource) {
return false;
}
@Override
public boolean onResourceReady(GlideDrawable resource, Object model, Target<GlideDrawable> target, boolean isFromMemoryCache, boolean isFirstResource) {
if (resource instanceof GlideBitmapDrawable) {
Log.w(TAG, "onResourceReady() for a Bitmap. Saving.");
attachment.setThumbnail(((GlideBitmapDrawable) resource).getBitmap());
}
LayoutParams layoutParams = (LayoutParams) getRemoveButton().getLayoutParams();
if (resource.getIntrinsicWidth() < getWidth()) {
layoutParams.topMargin = 0;
layoutParams.rightMargin = Math.max(0, (getWidth() - image.getPaddingRight() - resource.getIntrinsicWidth()) / 2);
} else {
layoutParams.topMargin = Math.max(0, (getHeight() - image.getPaddingTop() - resource.getIntrinsicHeight()) / 2);
layoutParams.rightMargin = 0;
}
getRemoveButton().setLayoutParams(layoutParams);
return false;
}
}
} }

View File

@ -279,7 +279,6 @@ public class MmsSmsDatabase extends Database {
return getCurrent(); return getCurrent();
} }
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
public MessageRecord getCurrent() { public MessageRecord getCurrent() {
String type = cursor.getString(cursor.getColumnIndexOrThrow(TRANSPORT)); String type = cursor.getString(cursor.getColumnIndexOrThrow(TRANSPORT));

View File

@ -34,6 +34,8 @@ import android.view.animation.Animation;
import android.widget.Toast; import android.widget.Toast;
import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.AudioView;
import org.thoughtcrime.securesms.components.RemovableMediaView;
import org.thoughtcrime.securesms.components.ThumbnailView; import org.thoughtcrime.securesms.components.ThumbnailView;
import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.providers.CaptureProvider; import org.thoughtcrime.securesms.providers.CaptureProvider;
@ -43,24 +45,29 @@ import org.thoughtcrime.securesms.util.MediaUtil;
import java.io.IOException; import java.io.IOException;
public class AttachmentManager { public class AttachmentManager {
private final static String TAG = AttachmentManager.class.getSimpleName(); private final static String TAG = AttachmentManager.class.getSimpleName();
private final Context context; private final @NonNull Context context;
private final View attachmentView; private final @NonNull View attachmentView;
private final ThumbnailView thumbnail; private final @NonNull RemovableMediaView removableMediaView;
private final SlideDeck slideDeck; private final @NonNull ThumbnailView thumbnail;
private final AttachmentListener attachmentListener; private final @NonNull AudioView audioView;
private final @NonNull SlideDeck slideDeck;
private final @NonNull AttachmentListener attachmentListener;
private Uri captureUri; private Uri captureUri;
public AttachmentManager(Activity view, AttachmentListener listener) { public AttachmentManager(@NonNull Activity view, @NonNull AttachmentListener listener) {
this.attachmentView = view.findViewById(R.id.attachment_editor); this.attachmentView = view.findViewById(R.id.attachment_editor);
this.thumbnail = (ThumbnailView) view.findViewById(R.id.attachment_thumbnail); this.thumbnail = (ThumbnailView) view.findViewById(R.id.attachment_thumbnail);
this.audioView = (AudioView) view.findViewById(R.id.attachment_audio);
this.removableMediaView = (RemovableMediaView) view.findViewById(R.id.removable_media_view);
this.slideDeck = new SlideDeck(); this.slideDeck = new SlideDeck();
this.context = view; this.context = view;
this.attachmentListener = listener; this.attachmentListener = listener;
thumbnail.setRemoveClickListener(new RemoveButtonListener()); removableMediaView.setRemoveClickListener(new RemoveButtonListener());
} }
public void clear() { public void clear() {
@ -81,6 +88,7 @@ public class AttachmentManager {
}); });
attachmentView.startAnimation(animation); attachmentView.startAnimation(animation);
audioView.cleanup();
} }
public void cleanup() { public void cleanup() {
@ -135,7 +143,15 @@ public class AttachmentManager {
} else { } else {
slideDeck.addSlide(slide); slideDeck.addSlide(slide);
attachmentView.setVisibility(View.VISIBLE); attachmentView.setVisibility(View.VISIBLE);
thumbnail.setImageResource(masterSecret, slide, false, true);
if (slide.hasAudio()) {
audioView.setAudio(masterSecret, (AudioSlide)slide, false);
removableMediaView.display(audioView);
} else {
thumbnail.setImageResource(masterSecret, slide, false);
removableMediaView.display(thumbnail);
}
attachmentListener.onAttachmentChanged(); attachmentListener.onAttachmentChanged();
} }
} }

View File

@ -0,0 +1,7 @@
package org.thoughtcrime.securesms.mms;
import android.view.View;
public interface SlideClickListener {
void onClick(View v, Slide slide);
}

View File

@ -81,6 +81,17 @@ public class SlideDeck {
return slide; return slide;
} }
} }
return null;
}
public @Nullable AudioSlide getAudioSlide() {
for (Slide slide : slides) {
if (slide.hasAudio()) {
return (AudioSlide)slide;
}
}
return null; return null;
} }
} }