diff --git a/res/layout/message_audio_view.xml b/res/layout/message_audio_view.xml index 0272a69e70..8ec2d8b64e 100644 --- a/res/layout/message_audio_view.xml +++ b/res/layout/message_audio_view.xml @@ -73,32 +73,33 @@ + app:wave_gap="1dp" + tools:wave_background_color="#bbb" + tools:wave_progress_color="?colorPrimary"/> + + - - \ No newline at end of file diff --git a/src/org/thoughtcrime/securesms/components/AudioViewOld.java b/src/org/thoughtcrime/securesms/components/AudioViewOld.java deleted file mode 100644 index f280cc4a73..0000000000 --- a/src/org/thoughtcrime/securesms/components/AudioViewOld.java +++ /dev/null @@ -1,331 +0,0 @@ -package org.thoughtcrime.securesms.components; - -import android.annotation.TargetApi; -import android.content.Context; -import android.content.res.ColorStateList; -import android.content.res.TypedArray; -import android.graphics.Color; -import android.graphics.PorterDuff; -import android.graphics.drawable.AnimatedVectorDrawable; -import android.os.Build; -import android.util.AttributeSet; -import android.view.MotionEvent; -import android.view.View; -import android.view.ViewGroup; -import android.widget.FrameLayout; -import android.widget.ImageView; -import android.widget.SeekBar; -import android.widget.TextView; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import com.pnikosis.materialishprogress.ProgressWheel; - -import org.greenrobot.eventbus.EventBus; -import org.greenrobot.eventbus.Subscribe; -import org.greenrobot.eventbus.ThreadMode; -import org.thoughtcrime.securesms.audio.AudioSlidePlayer; -import org.thoughtcrime.securesms.database.AttachmentDatabase; -import org.thoughtcrime.securesms.events.PartProgressEvent; -import org.thoughtcrime.securesms.logging.Log; -import org.thoughtcrime.securesms.mms.AudioSlide; -import org.thoughtcrime.securesms.mms.SlideClickListener; - -import java.io.IOException; -import java.util.concurrent.TimeUnit; - -import network.loki.messenger.R; - - -public class AudioViewOld extends FrameLayout implements AudioSlidePlayer.Listener { - - private static final String TAG = AudioViewOld.class.getSimpleName(); - - private final @NonNull AnimatingToggle controlToggle; - private final @NonNull ViewGroup container; - 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 AudioViewOld(Context context) { - this(context, null); - } - - public AudioViewOld(Context context, AttributeSet attrs) { - this(context, attrs, 0); - } - - public AudioViewOld(Context context, AttributeSet attrs, int defStyleAttr) { - super(context, attrs, defStyleAttr); - inflate(context, R.layout.message_audio_view, this); - - this.container = (ViewGroup) findViewById(R.id.audio_widget_container); - 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 (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - this.playButton.setImageDrawable(context.getDrawable(R.drawable.play_icon)); - this.pauseButton.setImageDrawable(context.getDrawable(R.drawable.pause_icon)); - this.playButton.setBackground(context.getDrawable(R.drawable.ic_circle_fill_white_48dp)); - this.pauseButton.setBackground(context.getDrawable(R.drawable.ic_circle_fill_white_48dp)); - } - - if (attrs != null) { - TypedArray typedArray = context.getTheme().obtainStyledAttributes(attrs, R.styleable.MessageAudioView, 0, 0); - setTint(typedArray.getColor(R.styleable.MessageAudioView_foregroundTintColor, Color.WHITE), - typedArray.getColor(R.styleable.MessageAudioView_backgroundTintColor, Color.WHITE)); - container.setBackgroundColor(typedArray.getColor(R.styleable.MessageAudioView_widgetBackground, Color.TRANSPARENT)); - typedArray.recycle(); - } - } - - @Override - protected void onAttachedToWindow() { - super.onAttachedToWindow(); - if (!EventBus.getDefault().isRegistered(this)) EventBus.getDefault().register(this); - } - - @Override - protected void onDetachedFromWindow() { - super.onDetachedFromWindow(); - EventBus.getDefault().unregister(this); - } - - public void setAudio(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(), 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 onPlayerStart(@NonNull AudioSlidePlayer player) { - if (this.pauseButton.getVisibility() != View.VISIBLE) { - togglePlayToPause(); - } - } - - @Override - public void onPlayerStop(@NonNull AudioSlidePlayer player) { - if (this.playButton.getVisibility() != View.VISIBLE) { - togglePauseToPlay(); - } - - if (seekBar.getProgress() + 5 >= seekBar.getMax()) { - backwardsCounter = 4; - onPlayerProgress(player, 0.0, 0); - } - } - - @Override - public void setFocusable(boolean focusable) { - super.setFocusable(focusable); - this.playButton.setFocusable(focusable); - this.pauseButton.setFocusable(focusable); - this.seekBar.setFocusable(focusable); - this.seekBar.setFocusableInTouchMode(focusable); - this.downloadButton.setFocusable(focusable); - } - - @Override - public void setClickable(boolean clickable) { - super.setClickable(clickable); - this.playButton.setClickable(clickable); - this.pauseButton.setClickable(clickable); - this.seekBar.setClickable(clickable); - this.seekBar.setOnTouchListener(clickable ? null : new TouchIgnoringListener()); - this.downloadButton.setClickable(clickable); - } - - @Override - public void setEnabled(boolean enabled) { - super.setEnabled(enabled); - this.playButton.setEnabled(enabled); - this.pauseButton.setEnabled(enabled); - this.seekBar.setEnabled(enabled); - this.downloadButton.setEnabled(enabled); - } - - @Override - public void onPlayerProgress(@NonNull AudioSlidePlayer player, 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 foregroundTint, int backgroundTint) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - this.playButton.setBackgroundTintList(ColorStateList.valueOf(foregroundTint)); - this.playButton.setImageTintList(ColorStateList.valueOf(backgroundTint)); - this.pauseButton.setBackgroundTintList(ColorStateList.valueOf(foregroundTint)); - this.pauseButton.setImageTintList(ColorStateList.valueOf(backgroundTint)); - } else { - this.playButton.setColorFilter(foregroundTint, PorterDuff.Mode.SRC_IN); - this.pauseButton.setColorFilter(foregroundTint, PorterDuff.Mode.SRC_IN); - } - - this.downloadButton.setColorFilter(foregroundTint, PorterDuff.Mode.SRC_IN); - this.downloadProgress.setBarColor(foregroundTint); - - this.timestamp.setTextColor(foregroundTint); - this.seekBar.getProgressDrawable().setColorFilter(foregroundTint, PorterDuff.Mode.SRC_IN); - this.seekBar.getThumb().setColorFilter(foregroundTint, 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 void togglePlayToPause() { - controlToggle.displayQuick(pauseButton); - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - AnimatedVectorDrawable playToPauseDrawable = (AnimatedVectorDrawable)getContext().getDrawable(R.drawable.play_to_pause_animation); - pauseButton.setImageDrawable(playToPauseDrawable); - playToPauseDrawable.start(); - } - } - - private void togglePauseToPlay() { - controlToggle.displayQuick(playButton); - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - AnimatedVectorDrawable pauseToPlayDrawable = (AnimatedVectorDrawable)getContext().getDrawable(R.drawable.pause_to_play_animation); - playButton.setImageDrawable(pauseToPlayDrawable); - pauseToPlayDrawable.start(); - } - } - - private class PlayClickedListener implements OnClickListener { - @TargetApi(Build.VERSION_CODES.LOLLIPOP) - @Override - public void onClick(View v) { - try { - Log.d(TAG, "playbutton onClick"); - if (audioSlidePlayer != null) { - togglePlayToPause(); - audioSlidePlayer.play(getProgress()); - } - } catch (IOException e) { - Log.w(TAG, e); - } - } - } - - private class PauseClickedListener implements OnClickListener { - @TargetApi(Build.VERSION_CODES.LOLLIPOP) - @Override - public void onClick(View v) { - Log.d(TAG, "pausebutton onClick"); - if (audioSlidePlayer != null) { - togglePauseToPlay(); - audioSlidePlayer.stop(); - } - } - } - - private class DownloadClickedListener implements 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); - } - } - } - - private static class TouchIgnoringListener implements OnTouchListener { - @Override - public boolean onTouch(View v, MotionEvent event) { - return true; - } - } - - @Subscribe(sticky = true, threadMode = ThreadMode.MAIN) - public void onEventAsync(final PartProgressEvent event) { - if (audioSlidePlayer != null && event.attachment.equals(audioSlidePlayer.getAudioSlide().asAttachment())) { - downloadProgress.setInstantProgress(((float) event.progress) / event.total); - } - } - -} diff --git a/src/org/thoughtcrime/securesms/loki/utilities/audio/DecodedAudio.java b/src/org/thoughtcrime/securesms/loki/utilities/audio/DecodedAudio.java index 72f83dea88..ef9067b54b 100644 --- a/src/org/thoughtcrime/securesms/loki/utilities/audio/DecodedAudio.java +++ b/src/org/thoughtcrime/securesms/loki/utilities/audio/DecodedAudio.java @@ -46,6 +46,7 @@ public class DecodedAudio { private final long mFileSize; private final int mAvgBitRate; // Average bit rate in kbps. private final int mSampleRate; + private final long mDuration; // In microseconds. private final int mChannels; private final int mNumSamples; // total number of samples per channel in audio file private final ShortBuffer mDecodedSamples; // shared buffer with mDecodedBytes. @@ -81,29 +82,31 @@ public class DecodedAudio { public DecodedAudio(MediaExtractor extractor, long size) throws IOException { mFileSize = size; + MediaFormat mediaFormat = null; int numTracks = extractor.getTrackCount(); // find and select the first audio track present in the file. - MediaFormat format = null; int trackIndex; for (trackIndex = 0; trackIndex < numTracks; trackIndex++) { - format = extractor.getTrackFormat(trackIndex); + MediaFormat format = extractor.getTrackFormat(trackIndex); if (format.getString(MediaFormat.KEY_MIME).startsWith("audio/")) { extractor.selectTrack(trackIndex); + mediaFormat = format; break; } } - if (trackIndex == numTracks) { + if (mediaFormat == null) { throw new IOException("No audio track found in the data source."); } - mChannels = format.getInteger(MediaFormat.KEY_CHANNEL_COUNT); - mSampleRate = format.getInteger(MediaFormat.KEY_SAMPLE_RATE); + mChannels = mediaFormat.getInteger(MediaFormat.KEY_CHANNEL_COUNT); + mSampleRate = mediaFormat.getInteger(MediaFormat.KEY_SAMPLE_RATE); + mDuration = mediaFormat.getLong(MediaFormat.KEY_DURATION); // Expected total number of samples per channel. int expectedNumSamples = - (int) ((format.getLong(MediaFormat.KEY_DURATION) / 1000000.f) * mSampleRate + 0.5f); + (int) ((mDuration / 1000000.f) * mSampleRate + 0.5f); - MediaCodec codec = MediaCodec.createDecoderByType(format.getString(MediaFormat.KEY_MIME)); - codec.configure(format, null, null, 0); + MediaCodec codec = MediaCodec.createDecoderByType(mediaFormat.getString(MediaFormat.KEY_MIME)); + codec.configure(mediaFormat, null, null, 0); codec.start(); try { @@ -135,7 +138,7 @@ public class DecodedAudio { if (!doneReading && inputBufferIndex >= 0) { sampleSize = extractor.readSampleData(codec.getInputBuffer(inputBufferIndex), 0); if (firstSampleData - && format.getString(MediaFormat.KEY_MIME).equals("audio/mp4a-latm") + && mediaFormat.getString(MediaFormat.KEY_MIME).equals("audio/mp4a-latm") && sampleSize == 2) { // For some reasons on some devices (e.g. the Samsung S3) you should not // provide the first two bytes of an AAC stream, otherwise the MediaCodec will @@ -285,6 +288,11 @@ public class DecodedAudio { return mChannels; } + /** @return Total duration in milliseconds. */ + public long getDuration() { + return mDuration; + } + public int getNumSamples() { return mNumSamples; // Number of samples per channel. } diff --git a/src/org/thoughtcrime/securesms/loki/views/MessageAudioView.kt b/src/org/thoughtcrime/securesms/loki/views/MessageAudioView.kt index 1ba800d6fb..d67bd1e236 100644 --- a/src/org/thoughtcrime/securesms/loki/views/MessageAudioView.kt +++ b/src/org/thoughtcrime/securesms/loki/views/MessageAudioView.kt @@ -14,8 +14,10 @@ import android.view.ViewGroup import android.widget.FrameLayout import android.widget.ImageView import android.widget.TextView +import androidx.annotation.ColorInt import androidx.annotation.RequiresApi import androidx.core.content.ContextCompat +import androidx.core.graphics.ColorUtils import com.pnikosis.materialishprogress.ProgressWheel import kotlinx.coroutines.* import network.loki.messenger.R @@ -37,6 +39,7 @@ import java.io.IOException import java.io.InputStream import java.lang.Exception import java.util.* +import java.util.concurrent.TimeUnit class MessageAudioView: FrameLayout, AudioSlidePlayer.Listener { @@ -51,7 +54,7 @@ class MessageAudioView: FrameLayout, AudioSlidePlayer.Listener { private val downloadButton: ImageView private val downloadProgress: ProgressWheel private val seekBar: WaveformSeekBar - private val timestamp: TextView + private val totalDuration: TextView private var downloadListener: SlideClickListener? = null private var audioSlidePlayer: AudioSlidePlayer? = null @@ -73,7 +76,7 @@ class MessageAudioView: FrameLayout, AudioSlidePlayer.Listener { downloadButton = findViewById(R.id.download) downloadProgress = findViewById(R.id.download_progress) seekBar = findViewById(R.id.seek) - timestamp = findViewById(R.id.timestamp) + totalDuration = findViewById(R.id.total_duration) playButton.setOnClickListener { try { @@ -158,6 +161,7 @@ class MessageAudioView: FrameLayout, AudioSlidePlayer.Listener { if (downloadProgress.isSpinning) { downloadProgress.stopSpinning() } + // Post to make sure it executes only when the view is attached to a window. post(::updateSeekBarFromAudio) } @@ -175,7 +179,7 @@ class MessageAudioView: FrameLayout, AudioSlidePlayer.Listener { downloadListener = listener } - fun setTint(foregroundTint: Int, backgroundTint: Int) { + fun setTint(@ColorInt foregroundTint: Int, @ColorInt backgroundTint: Int) { playButton.backgroundTintList = ColorStateList.valueOf(foregroundTint) playButton.imageTintList = ColorStateList.valueOf(backgroundTint) pauseButton.backgroundTintList = ColorStateList.valueOf(foregroundTint) @@ -183,11 +187,13 @@ class MessageAudioView: FrameLayout, AudioSlidePlayer.Listener { downloadButton.setColorFilter(foregroundTint, PorterDuff.Mode.SRC_IN) downloadProgress.barColor = foregroundTint - timestamp.setTextColor(foregroundTint) + totalDuration.setTextColor(foregroundTint) // val colorFilter = createBlendModeColorFilterCompat(foregroundTint, BlendModeCompat.SRC_IN) // seekBar.progressDrawable.colorFilter = colorFilter // seekBar.thumb.colorFilter = colorFilter + seekBar.waveProgressColor = foregroundTint + seekBar.waveBackgroundColor = ColorUtils.blendARGB(foregroundTint, backgroundTint, 0.75f) } override fun onPlayerStart(player: AudioSlidePlayer) { @@ -284,24 +290,36 @@ class MessageAudioView: FrameLayout, AudioSlidePlayer.Listener { return Random(seed.toLong()).let { (0 until frames).map { i -> it.nextFloat() }.toFloatArray() } } - val rmsValues: FloatArray + var rmsValues: FloatArray = floatArrayOf() + var totalDurationMs: Long = -1 - rmsValues = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { // Due to API version incompatibility, we just display some random waveform for older API. - generateFakeRms(extractAttachmentRandomSeed(attachment)) + rmsValues = generateFakeRms(extractAttachmentRandomSeed(attachment)) } else { try { @Suppress("BlockingMethodInNonBlockingContext") - PartAuthority.getAttachmentStream(context, attachment.dataUri!!).use { - DecodedAudio(InputStreamMediaDataSource(it)).calculateRms(rmsFrames) + val decodedAudio = PartAuthority.getAttachmentStream(context, attachment.dataUri!!).use { + DecodedAudio(InputStreamMediaDataSource(it)) } + rmsValues = decodedAudio.calculateRms(rmsFrames) + totalDurationMs = (decodedAudio.duration / 1000.0).toLong() } catch (e: Exception) { android.util.Log.w(TAG, "Failed to decode sample values for the audio attachment \"${attachment.fileName}\".", e) - generateFakeRms(extractAttachmentRandomSeed(attachment)) + rmsValues = generateFakeRms(extractAttachmentRandomSeed(attachment)) } } - post { seekBar.sample = rmsValues } + post { + seekBar.sample = rmsValues + + if (totalDurationMs > 0) { + totalDuration.visibility = View.VISIBLE + totalDuration.text = String.format("%02d:%02d", + TimeUnit.MILLISECONDS.toMinutes(totalDurationMs), + TimeUnit.MILLISECONDS.toSeconds(totalDurationMs)) + } + } } } diff --git a/src/org/thoughtcrime/securesms/loki/views/WaveformSeekBar.kt b/src/org/thoughtcrime/securesms/loki/views/WaveformSeekBar.kt index 8434186c2d..6d562fc6ba 100644 --- a/src/org/thoughtcrime/securesms/loki/views/WaveformSeekBar.kt +++ b/src/org/thoughtcrime/securesms/loki/views/WaveformSeekBar.kt @@ -80,21 +80,13 @@ class WaveformSeekBar : View { invalidate() } - var waveGap: Float = - dp( - context, - 2f - ) + var waveGap: Float = dp(context, 2f) set(value) { field = value invalidate() } - var waveWidth: Float = - dp( - context, - 5f - ) + var waveWidth: Float = dp(context, 5f) set(value) { field = value invalidate() @@ -106,11 +98,7 @@ class WaveformSeekBar : View { invalidate() } - var waveCornerRadius: Float = - dp( - context, - 2.5f - ) + var waveCornerRadius: Float = dp(context, 2.5f) set(value) { field = value invalidate() @@ -235,24 +223,26 @@ class WaveformSeekBar : View { when (event.action) { MotionEvent.ACTION_DOWN -> { userSeeking = true +// preUserSeekingProgress = _progress if (isParentScrolling()) { touchDownX = event.x } else { - updateProgress(event, true) + updateProgress(event, false) } } MotionEvent.ACTION_MOVE -> { - updateProgress(event, true) + updateProgress(event, false) } MotionEvent.ACTION_UP -> { userSeeking = false if (abs(event.x - touchDownX) > scaledTouchSlop) { - updateProgress(event, false) + updateProgress(event, true) } performClick() } MotionEvent.ACTION_CANCEL -> { userSeeking = false +// updateProgress(preUserSeekingProgress, false) } } return true @@ -276,19 +266,32 @@ class WaveformSeekBar : View { } } - private fun updateProgress(event: MotionEvent, delayNotification: Boolean) { - _progress = event.x / getAvailableWith() + private fun updateProgress(event: MotionEvent, notify: Boolean) { + updateProgress(event.x / getAvailableWith(), notify) + } + + private fun updateProgress(progress: Float, notify: Boolean) { + _progress = progress invalidate() - postponedProgressUpdateHandler.removeCallbacks(postponedProgressUpdateRunnable) - if (delayNotification) { - // Re-post delayed user update notification to throttle a bit. - postponedProgressUpdateHandler.postDelayed(postponedProgressUpdateRunnable, 150) - } else { + if (notify) { postponedProgressUpdateRunnable.run() } } +// private fun updateProgress(event: MotionEvent, delayNotification: Boolean) { +// _progress = event.x / getAvailableWith() +// invalidate() +// +// postponedProgressUpdateHandler.removeCallbacks(postponedProgressUpdateRunnable) +// if (delayNotification) { +// // Re-post delayed user update notification to throttle a bit. +// postponedProgressUpdateHandler.postDelayed(postponedProgressUpdateRunnable, 150) +// } else { +// postponedProgressUpdateRunnable.run() +// } +// } + override fun performClick(): Boolean { super.performClick() return true