mirror of
https://github.com/oxen-io/session-android.git
synced 2025-10-24 07:59:14 +00:00
Support for an audio view to allow in-app playback of audio.
Closes #4270 // FREEBIE
This commit is contained in:
@@ -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
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user