Support for an audio view to allow in-app playback of audio.
Closes #4270 // FREEBIE
BIN
res/drawable-hdpi/ic_download_circle_fill_white_48dp.png
Normal file
After Width: | Height: | Size: 2.4 KiB |
BIN
res/drawable-hdpi/ic_pause_circle_fill_white_48dp.png
Normal file
After Width: | Height: | Size: 831 B |
BIN
res/drawable-hdpi/ic_play_circle_fill_white_48dp.png
Normal file
After Width: | Height: | Size: 786 B |
BIN
res/drawable-mdpi/ic_download_circle_fill_white_48dp.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
BIN
res/drawable-mdpi/ic_pause_circle_fill_white_48dp.png
Normal file
After Width: | Height: | Size: 524 B |
BIN
res/drawable-mdpi/ic_play_circle_fill_white_48dp.png
Normal file
After Width: | Height: | Size: 559 B |
BIN
res/drawable-xhdpi/ic_download_circle_fill_white_48dp.png
Normal file
After Width: | Height: | Size: 3.1 KiB |
BIN
res/drawable-xhdpi/ic_pause_circle_fill_white_48dp.png
Normal file
After Width: | Height: | Size: 959 B |
BIN
res/drawable-xhdpi/ic_play_circle_fill_white_48dp.png
Normal file
After Width: | Height: | Size: 1 KiB |
BIN
res/drawable-xxhdpi/ic_download_circle_fill_white_48dp.png
Normal file
After Width: | Height: | Size: 5.4 KiB |
BIN
res/drawable-xxhdpi/ic_pause_circle_fill_white_48dp.png
Normal file
After Width: | Height: | Size: 1.7 KiB |
BIN
res/drawable-xxhdpi/ic_play_circle_fill_white_48dp.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
BIN
res/drawable-xxxhdpi/ic_download_circle_fill_white_48dp.png
Normal file
After Width: | Height: | Size: 2.4 KiB |
BIN
res/drawable-xxxhdpi/ic_pause_circle_fill_white_48dp.png
Normal file
After Width: | Height: | Size: 2.3 KiB |
BIN
res/drawable-xxxhdpi/ic_play_circle_fill_white_48dp.png
Normal file
After Width: | Height: | Size: 2.1 KiB |
91
res/layout/audio_view.xml
Normal 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>
|
|
@ -37,13 +37,32 @@
|
|||
android:background="?android:windowBackground"
|
||||
android:visibility="gone">
|
||||
|
||||
<org.thoughtcrime.securesms.components.ThumbnailView
|
||||
android:id="@+id/attachment_thumbnail"
|
||||
android:layout_width="230dp"
|
||||
android:layout_height="150dp"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:contentDescription="@string/conversation_activity__attachment_thumbnail"
|
||||
app:backgroundColorHint="?conversation_background" />
|
||||
<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
|
||||
android:id="@+id/attachment_thumbnail"
|
||||
android:layout_width="230dp"
|
||||
android:layout_height="150dp"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:visibility="gone"
|
||||
android:contentDescription="@string/conversation_activity__attachment_thumbnail"
|
||||
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>
|
||||
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<org.thoughtcrime.securesms.ConversationItem android:id="@+id/conversation_item"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingRight="10dip"
|
||||
android:orientation="vertical"
|
||||
android:background="@drawable/conversation_item_background"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
<org.thoughtcrime.securesms.ConversationItem
|
||||
android:id="@+id/conversation_item"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingRight="10dip"
|
||||
android:orientation="vertical"
|
||||
android:background="@drawable/conversation_item_background"
|
||||
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">
|
||||
|
||||
<TextView android:id="@+id/group_message_status"
|
||||
android:layout_width="wrap_content"
|
||||
|
@ -53,7 +55,15 @@
|
|||
android:contentDescription="@string/conversation_item__mms_image_description"
|
||||
android:visibility="gone"
|
||||
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
|
||||
android:id="@+id/conversation_item_body"
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<org.thoughtcrime.securesms.ConversationItem
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/conversation_item"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:background="@drawable/conversation_item_background">
|
||||
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:id="@+id/conversation_item"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:background="@drawable/conversation_item_background">
|
||||
|
||||
<RelativeLayout android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
|
@ -67,6 +68,13 @@
|
|||
tools:src="@drawable/ic_video_light"
|
||||
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
|
||||
android:id="@+id/conversation_item_body"
|
||||
android:autoLink="all"
|
||||
|
|
|
@ -3,4 +3,6 @@
|
|||
android:id="@+id/remove_image_button"
|
||||
android:layout_width="@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"/>
|
|
@ -16,9 +16,4 @@
|
|||
android:layout_gravity="center"
|
||||
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>
|
||||
|
|
|
@ -134,4 +134,9 @@
|
|||
<declare-styleable name="ThumbnailView">
|
||||
<attr name="backgroundColorHint" format="color" />
|
||||
</declare-styleable>
|
||||
|
||||
<declare-styleable name="AudioView">
|
||||
<attr name="tintColor" format="color" />
|
||||
</declare-styleable>
|
||||
|
||||
</resources>
|
||||
|
|
|
@ -4,6 +4,7 @@ import android.support.annotation.NonNull;
|
|||
|
||||
import org.thoughtcrime.securesms.crypto.MasterSecret;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.recipients.Recipients;
|
||||
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
|
@ -13,5 +14,5 @@ public interface BindableConversationItem extends Unbindable {
|
|||
@NonNull MessageRecord messageRecord,
|
||||
@NonNull Locale locale,
|
||||
@NonNull Set<MessageRecord> batchSelected,
|
||||
boolean groupThread);
|
||||
@NonNull Recipients recipients);
|
||||
}
|
||||
|
|
|
@ -35,6 +35,7 @@ import org.thoughtcrime.securesms.database.MmsSmsColumns;
|
|||
import org.thoughtcrime.securesms.database.MmsSmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.SmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.recipients.Recipients;
|
||||
import org.thoughtcrime.securesms.util.LRUCache;
|
||||
|
||||
import java.lang.ref.SoftReference;
|
||||
|
@ -68,12 +69,12 @@ public class ConversationAdapter <V extends View & BindableConversationItem>
|
|||
|
||||
private final Set<MessageRecord> batchSelected = Collections.synchronizedSet(new HashSet<MessageRecord>());
|
||||
|
||||
private final ItemClickListener clickListener;
|
||||
private final MasterSecret masterSecret;
|
||||
private final Locale locale;
|
||||
private final boolean groupThread;
|
||||
private final MmsSmsDatabase db;
|
||||
private final LayoutInflater inflater;
|
||||
private final ItemClickListener clickListener;
|
||||
private final MasterSecret masterSecret;
|
||||
private final Locale locale;
|
||||
private final Recipients recipients;
|
||||
private final MmsSmsDatabase db;
|
||||
private final LayoutInflater inflater;
|
||||
|
||||
protected static class ViewHolder extends RecyclerView.ViewHolder {
|
||||
public <V extends View & BindableConversationItem> ViewHolder(final @NonNull V itemView) {
|
||||
|
@ -96,15 +97,15 @@ public class ConversationAdapter <V extends View & BindableConversationItem>
|
|||
@NonNull Locale locale,
|
||||
@Nullable ItemClickListener clickListener,
|
||||
@Nullable Cursor cursor,
|
||||
boolean groupThread)
|
||||
@NonNull Recipients recipients)
|
||||
{
|
||||
super(context, cursor);
|
||||
this.masterSecret = masterSecret;
|
||||
this.locale = locale;
|
||||
this.clickListener = clickListener;
|
||||
this.groupThread = groupThread;
|
||||
this.inflater = LayoutInflater.from(context);
|
||||
this.db = DatabaseFactory.getMmsSmsDatabase(context);
|
||||
this.masterSecret = masterSecret;
|
||||
this.locale = locale;
|
||||
this.clickListener = clickListener;
|
||||
this.recipients = recipients;
|
||||
this.inflater = LayoutInflater.from(context);
|
||||
this.db = DatabaseFactory.getMmsSmsDatabase(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -118,7 +119,7 @@ public class ConversationAdapter <V extends View & BindableConversationItem>
|
|||
String type = cursor.getString(cursor.getColumnIndexOrThrow(MmsSmsDatabase.TRANSPORT));
|
||||
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) {
|
||||
|
|
|
@ -142,8 +142,7 @@ public class ConversationFragment extends Fragment
|
|||
|
||||
private void initializeListAdapter() {
|
||||
if (this.recipients != null && this.threadId != -1) {
|
||||
list.setAdapter(new ConversationAdapter(getActivity(), masterSecret, locale, selectionClickListener, null,
|
||||
(!this.recipients.isSingleRecipient()) || this.recipients.isGroupRecipient()));
|
||||
list.setAdapter(new ConversationAdapter(getActivity(), masterSecret, locale, selectionClickListener, null, this.recipients));
|
||||
getLoaderManager().restartLoader(0, Bundle.EMPTY, this);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -41,13 +41,14 @@ import android.widget.Toast;
|
|||
|
||||
import com.afollestad.materialdialogs.AlertDialogWrapper;
|
||||
|
||||
import org.thoughtcrime.securesms.components.AudioView;
|
||||
import org.thoughtcrime.securesms.components.AvatarImageView;
|
||||
import org.thoughtcrime.securesms.components.ThumbnailView;
|
||||
import org.thoughtcrime.securesms.crypto.MasterSecret;
|
||||
import org.thoughtcrime.securesms.database.AttachmentDatabase;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.MmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.MmsSmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.AttachmentDatabase;
|
||||
import org.thoughtcrime.securesms.database.SmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch;
|
||||
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.mms.PartAuthority;
|
||||
import org.thoughtcrime.securesms.mms.Slide;
|
||||
import org.thoughtcrime.securesms.mms.SlideClickListener;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.Recipients;
|
||||
import org.thoughtcrime.securesms.util.DateUtils;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
|
@ -75,7 +79,7 @@ import java.util.Set;
|
|||
*/
|
||||
|
||||
public class ConversationItem extends LinearLayout
|
||||
implements Recipient.RecipientModifiedListener, BindableConversationItem
|
||||
implements Recipient.RecipientModifiedListener, Recipients.RecipientsModifiedListener, BindableConversationItem
|
||||
{
|
||||
private final static String TAG = ConversationItem.class.getSimpleName();
|
||||
|
||||
|
@ -98,11 +102,13 @@ public class ConversationItem extends LinearLayout
|
|||
private View pendingIndicator;
|
||||
private ImageView pendingApprovalIndicator;
|
||||
|
||||
private StatusManager statusManager;
|
||||
private Set<MessageRecord> batchSelected;
|
||||
private ThumbnailView mediaThumbnail;
|
||||
private Button mmsDownloadButton;
|
||||
private TextView mmsDownloadingLabel;
|
||||
private @NonNull Set<MessageRecord> batchSelected = new HashSet<>();
|
||||
private @Nullable Recipients conversationRecipients;
|
||||
private @NonNull StatusManager statusManager;
|
||||
private @NonNull ThumbnailView mediaThumbnail;
|
||||
private @NonNull AudioView audioView;
|
||||
private @NonNull Button mmsDownloadButton;
|
||||
private @NonNull TextView mmsDownloadingLabel;
|
||||
|
||||
private int defaultBubbleColor;
|
||||
|
||||
|
@ -152,15 +158,20 @@ public class ConversationItem extends LinearLayout
|
|||
this.pendingApprovalIndicator = (ImageView) findViewById(R.id.pending_approval_indicator);
|
||||
this.pendingIndicator = findViewById(R.id.pending_indicator);
|
||||
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);
|
||||
|
||||
setOnClickListener(new ClickListener(null));
|
||||
PassthroughClickListener passthroughClickListener = new PassthroughClickListener();
|
||||
if (mmsDownloadButton != null) mmsDownloadButton.setOnClickListener(mmsDownloadClickListener);
|
||||
PassthroughClickListener passthroughClickListener = new PassthroughClickListener();
|
||||
AttachmentDownloadClickListener downloadClickListener = new AttachmentDownloadClickListener();
|
||||
|
||||
mmsDownloadButton.setOnClickListener(mmsDownloadClickListener);
|
||||
mediaThumbnail.setThumbnailClickListener(new ThumbnailClickListener());
|
||||
mediaThumbnail.setDownloadClickListener(new ThumbnailDownloadClickListener());
|
||||
mediaThumbnail.setDownloadClickListener(downloadClickListener);
|
||||
mediaThumbnail.setOnLongClickListener(passthroughClickListener);
|
||||
mediaThumbnail.setOnClickListener(passthroughClickListener);
|
||||
audioView.setDownloadClickListener(downloadClickListener);
|
||||
audioView.setOnLongClickListener(passthroughClickListener);
|
||||
bodyText.setOnLongClickListener(passthroughClickListener);
|
||||
bodyText.setOnClickListener(passthroughClickListener);
|
||||
}
|
||||
|
@ -170,16 +181,18 @@ public class ConversationItem extends LinearLayout
|
|||
@NonNull MessageRecord messageRecord,
|
||||
@NonNull Locale locale,
|
||||
@NonNull Set<MessageRecord> batchSelected,
|
||||
boolean groupThread)
|
||||
@NonNull Recipients conversationRecipients)
|
||||
{
|
||||
this.masterSecret = masterSecret;
|
||||
this.messageRecord = messageRecord;
|
||||
this.locale = locale;
|
||||
this.batchSelected = batchSelected;
|
||||
this.groupThread = groupThread;
|
||||
this.conversationRecipients = conversationRecipients;
|
||||
this.groupThread = !conversationRecipients.isSingleRecipient() || conversationRecipients.isGroupRecipient();
|
||||
this.recipient = messageRecord.getIndividualRecipient();
|
||||
|
||||
this.recipient.addListener(this);
|
||||
this.conversationRecipients.addListener(this);
|
||||
|
||||
setInteractionState(messageRecord);
|
||||
setBodyText(messageRecord);
|
||||
|
@ -218,6 +231,7 @@ public class ConversationItem extends LinearLayout
|
|||
if (messageRecord.isOutgoing()) {
|
||||
bodyBubble.getBackground().setColorFilter(defaultBubbleColor, PorterDuff.Mode.MULTIPLY);
|
||||
mediaThumbnail.setBackgroundColorHint(defaultBubbleColor);
|
||||
audioView.setTint(conversationRecipients.getColor().toConversationColor(context));
|
||||
} else {
|
||||
int color = recipient.getColor().toConversationColor(context);
|
||||
bodyBubble.getBackground().setColorFilter(color, PorterDuff.Mode.MULTIPLY);
|
||||
|
@ -237,7 +251,13 @@ public class ConversationItem extends LinearLayout
|
|||
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() &&
|
||||
!messageRecord.isMmsNotification() &&
|
||||
((MediaMmsMessageRecord)messageRecord).getSlideDeck().getThumbnailSlide() != null;
|
||||
|
@ -256,20 +276,33 @@ public class ConversationItem extends LinearLayout
|
|||
}
|
||||
|
||||
private void setMediaAttributes(MessageRecord messageRecord) {
|
||||
boolean showControls = !messageRecord.isFailed() && (!messageRecord.isOutgoing() || messageRecord.isPending());
|
||||
|
||||
if (messageRecord.isMmsNotification()) {
|
||||
mediaThumbnail.setVisibility(View.GONE);
|
||||
audioView.setVisibility(View.GONE);
|
||||
|
||||
bodyText.setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT));
|
||||
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);
|
||||
audioView.setVisibility(View.GONE);
|
||||
|
||||
//noinspection ConstantConditions
|
||||
mediaThumbnail.setImageResource(masterSecret,
|
||||
((MediaMmsMessageRecord)messageRecord).getSlideDeck().getThumbnailSlide(),
|
||||
!messageRecord.isFailed() && (!messageRecord.isOutgoing() || messageRecord.isPending()),
|
||||
false);
|
||||
showControls);
|
||||
bodyText.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
|
||||
} else {
|
||||
mediaThumbnail.setVisibility(View.GONE);
|
||||
audioView.setVisibility(View.GONE);
|
||||
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) {
|
||||
DatabaseFactory.getAttachmentDatabase(context).setTransferState(messageRecord.getId(),
|
||||
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) {
|
||||
Log.w(TAG, "Clicked: " + slide.getUri() + " , " + slide.getContentType());
|
||||
Intent intent = new Intent(Intent.ACTION_VIEW);
|
||||
|
|
|
@ -56,7 +56,7 @@ public class ConversationUpdateItem extends LinearLayout
|
|||
@NonNull MessageRecord messageRecord,
|
||||
@NonNull Locale locale,
|
||||
@NonNull Set<MessageRecord> batchSelected,
|
||||
boolean groupThread)
|
||||
@NonNull Recipients conversationRecipients)
|
||||
{
|
||||
bind(messageRecord, locale);
|
||||
}
|
||||
|
|
|
@ -70,7 +70,7 @@ public class ImageMediaAdapter extends CursorRecyclerViewAdapter<ViewHolder> {
|
|||
Slide slide = MediaUtil.getSlideForAttachment(getContext(), imageRecord.getAttachment());
|
||||
|
||||
if (slide != null) {
|
||||
imageView.setImageResource(masterSecret, slide, false, false);
|
||||
imageView.setImageResource(masterSecret, slide, false);
|
||||
}
|
||||
|
||||
imageView.setOnClickListener(new OnMediaClickListener(imageRecord));
|
||||
|
|
|
@ -172,8 +172,7 @@ public class MessageDetailsActivity extends PassphraseRequiredActionBarActivity
|
|||
}
|
||||
toFrom.setText(toFromRes);
|
||||
conversationItem.bind(masterSecret, messageRecord, dynamicLanguage.getCurrentLocale(),
|
||||
new HashSet<MessageRecord>(),
|
||||
recipients != messageRecord.getRecipients());
|
||||
new HashSet<MessageRecord>(), recipients);
|
||||
recipientsList.setAdapter(new MessageDetailsRecipientAdapter(this, masterSecret, messageRecord,
|
||||
recipients, isPushGroup));
|
||||
}
|
||||
|
|
|
@ -13,15 +13,15 @@ import java.io.InputStream;
|
|||
|
||||
public class UriAttachment extends Attachment {
|
||||
|
||||
private final Uri dataUri;
|
||||
private final Uri thumbnailUri;
|
||||
private final @NonNull Uri dataUri;
|
||||
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);
|
||||
}
|
||||
|
||||
public UriAttachment(Uri dataUri, Uri thumbnailUri,
|
||||
String contentType, int transferState, long size)
|
||||
public UriAttachment(@NonNull Uri dataUri, @NonNull Uri thumbnailUri,
|
||||
@NonNull String contentType, int transferState, long size)
|
||||
{
|
||||
super(contentType, transferState, size, null, null, null);
|
||||
this.dataUri = dataUri;
|
||||
|
@ -39,4 +39,14 @@ public class UriAttachment extends Attachment {
|
|||
public Uri getThumbnailUri() {
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
|
376
src/org/thoughtcrime/securesms/audio/AudioAttachmentServer.java
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
237
src/org/thoughtcrime/securesms/audio/AudioSlidePlayer.java
Normal 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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -57,4 +57,12 @@ public class AnimatingToggle extends FrameLayout {
|
|||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
233
src/org/thoughtcrime/securesms/components/AudioView.java
Normal 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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -17,35 +17,31 @@ import android.widget.ImageView;
|
|||
import com.bumptech.glide.DrawableRequestBuilder;
|
||||
import com.bumptech.glide.GenericRequestBuilder;
|
||||
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.attachments.Attachment;
|
||||
import org.thoughtcrime.securesms.crypto.MasterSecret;
|
||||
import org.thoughtcrime.securesms.database.AttachmentDatabase;
|
||||
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri;
|
||||
import org.thoughtcrime.securesms.mms.RoundedCorners;
|
||||
import org.thoughtcrime.securesms.mms.Slide;
|
||||
import org.thoughtcrime.securesms.mms.SlideClickListener;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
import org.whispersystems.libaxolotl.util.guava.Optional;
|
||||
|
||||
public class ThumbnailView extends FrameLayout {
|
||||
|
||||
private static final String TAG = ThumbnailView.class.getSimpleName();
|
||||
|
||||
private ImageView image;
|
||||
private ImageView removeButton;
|
||||
private int backgroundColorHint;
|
||||
private int radius;
|
||||
private OnClickListener parentClickListener;
|
||||
|
||||
private Optional<TransferControlView> transferControls = Optional.absent();
|
||||
private ThumbnailClickListener thumbnailClickListener = null;
|
||||
private ThumbnailClickListener downloadClickListener = null;
|
||||
private Slide slide = null;
|
||||
private Optional<TransferControlView> transferControls = Optional.absent();
|
||||
private SlideClickListener thumbnailClickListener = null;
|
||||
private SlideClickListener downloadClickListener = null;
|
||||
private Slide slide = null;
|
||||
|
||||
public ThumbnailView(Context context) {
|
||||
this(context, null);
|
||||
|
@ -57,9 +53,11 @@ public class ThumbnailView extends FrameLayout {
|
|||
|
||||
public ThumbnailView(final Context context, AttributeSet attrs, int defStyle) {
|
||||
super(context, attrs, defStyle);
|
||||
|
||||
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());
|
||||
|
||||
if (attrs != null) {
|
||||
|
@ -86,21 +84,6 @@ public class ThumbnailView extends FrameLayout {
|
|||
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() {
|
||||
if (!transferControls.isPresent()) {
|
||||
transferControls = Optional.of((TransferControlView)ViewUtil.inflateStub(this, R.id.transfer_controls_stub));
|
||||
|
@ -112,9 +95,8 @@ public class ThumbnailView extends FrameLayout {
|
|||
this.backgroundColorHint = color;
|
||||
}
|
||||
|
||||
public void setImageResource(@NonNull MasterSecret masterSecret, @NonNull Slide slide,
|
||||
boolean showControls, boolean showRemove)
|
||||
{
|
||||
public void setImageResource(@NonNull MasterSecret masterSecret, @NonNull Slide slide, boolean showControls) {
|
||||
|
||||
if (Util.equals(slide, this.slide)) {
|
||||
Log.w(TAG, "Not re-loading slide " + slide.asAttachment().getDataUri());
|
||||
return;
|
||||
|
@ -137,22 +119,16 @@ public class ThumbnailView extends FrameLayout {
|
|||
|
||||
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 Glide.clear(image);
|
||||
}
|
||||
|
||||
public void setThumbnailClickListener(ThumbnailClickListener listener) {
|
||||
public void setThumbnailClickListener(SlideClickListener listener) {
|
||||
this.thumbnailClickListener = listener;
|
||||
}
|
||||
|
||||
public void setRemoveClickListener(OnClickListener 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) {
|
||||
public void setDownloadClickListener(SlideClickListener listener) {
|
||||
this.downloadClickListener = listener;
|
||||
}
|
||||
|
||||
|
@ -174,15 +150,11 @@ public class ThumbnailView extends FrameLayout {
|
|||
!((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()))
|
||||
.crossFade()
|
||||
.transform(new RoundedCorners(getContext(), true, radius, backgroundColorHint));
|
||||
|
||||
if (showRemove) {
|
||||
builder = builder.listener(new ThumbnailSetListener(slide.asAttachment()));
|
||||
}
|
||||
|
||||
if (slide.isInProgress()) return builder;
|
||||
else return builder.error(R.drawable.ic_missing_thumbnail_picture);
|
||||
}
|
||||
|
@ -193,10 +165,6 @@ public class ThumbnailView extends FrameLayout {
|
|||
.fitCenter();
|
||||
}
|
||||
|
||||
public interface ThumbnailClickListener {
|
||||
void onClick(View v, Slide slide);
|
||||
}
|
||||
|
||||
private class ThumbnailClickDispatcher implements View.OnClickListener {
|
||||
@Override
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -279,7 +279,6 @@ public class MmsSmsDatabase extends Database {
|
|||
return getCurrent();
|
||||
}
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
|
||||
public MessageRecord getCurrent() {
|
||||
String type = cursor.getString(cursor.getColumnIndexOrThrow(TRANSPORT));
|
||||
|
||||
|
|
|
@ -34,6 +34,8 @@ import android.view.animation.Animation;
|
|||
import android.widget.Toast;
|
||||
|
||||
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.crypto.MasterSecret;
|
||||
import org.thoughtcrime.securesms.providers.CaptureProvider;
|
||||
|
@ -43,24 +45,29 @@ import org.thoughtcrime.securesms.util.MediaUtil;
|
|||
import java.io.IOException;
|
||||
|
||||
public class AttachmentManager {
|
||||
|
||||
private final static String TAG = AttachmentManager.class.getSimpleName();
|
||||
|
||||
private final Context context;
|
||||
private final View attachmentView;
|
||||
private final ThumbnailView thumbnail;
|
||||
private final SlideDeck slideDeck;
|
||||
private final AttachmentListener attachmentListener;
|
||||
private final @NonNull Context context;
|
||||
private final @NonNull View attachmentView;
|
||||
private final @NonNull RemovableMediaView removableMediaView;
|
||||
private final @NonNull ThumbnailView thumbnail;
|
||||
private final @NonNull AudioView audioView;
|
||||
private final @NonNull SlideDeck slideDeck;
|
||||
private final @NonNull AttachmentListener attachmentListener;
|
||||
|
||||
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.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.context = view;
|
||||
this.attachmentListener = listener;
|
||||
|
||||
thumbnail.setRemoveClickListener(new RemoveButtonListener());
|
||||
removableMediaView.setRemoveClickListener(new RemoveButtonListener());
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
|
@ -81,6 +88,7 @@ public class AttachmentManager {
|
|||
});
|
||||
|
||||
attachmentView.startAnimation(animation);
|
||||
audioView.cleanup();
|
||||
}
|
||||
|
||||
public void cleanup() {
|
||||
|
@ -135,7 +143,15 @@ public class AttachmentManager {
|
|||
} else {
|
||||
slideDeck.addSlide(slide);
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
package org.thoughtcrime.securesms.mms;
|
||||
|
||||
import android.view.View;
|
||||
|
||||
public interface SlideClickListener {
|
||||
void onClick(View v, Slide slide);
|
||||
}
|
|
@ -81,6 +81,17 @@ public class SlideDeck {
|
|||
return slide;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public @Nullable AudioSlide getAudioSlide() {
|
||||
for (Slide slide : slides) {
|
||||
if (slide.hasAudio()) {
|
||||
return (AudioSlide)slide;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|