From fdaadcb2b0991e68e55dea479a87b616cbe68f00 Mon Sep 17 00:00:00 2001 From: Anton Chekulaev Date: Fri, 2 Oct 2020 13:32:42 +1000 Subject: [PATCH] Audio view ported to Kotlin. --- .../securesms/components/AudioView.java | 330 ------------------ .../securesms/components/AudioView.kt | 267 ++++++++++++++ 2 files changed, 267 insertions(+), 330 deletions(-) delete mode 100644 src/org/thoughtcrime/securesms/components/AudioView.java create mode 100644 src/org/thoughtcrime/securesms/components/AudioView.kt diff --git a/src/org/thoughtcrime/securesms/components/AudioView.java b/src/org/thoughtcrime/securesms/components/AudioView.java deleted file mode 100644 index 9e4c7c3e9a..0000000000 --- a/src/org/thoughtcrime/securesms/components/AudioView.java +++ /dev/null @@ -1,330 +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 androidx.annotation.NonNull; -import androidx.annotation.Nullable; -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 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 AudioView extends FrameLayout implements AudioSlidePlayer.Listener { - - private static final String TAG = AudioView.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 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.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.AudioView, 0, 0); - setTint(typedArray.getColor(R.styleable.AudioView_foregroundTintColor, Color.WHITE), - typedArray.getColor(R.styleable.AudioView_backgroundTintColor, Color.WHITE)); - container.setBackgroundColor(typedArray.getColor(R.styleable.AudioView_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 onStart() { - if (this.pauseButton.getVisibility() != View.VISIBLE) { - togglePlayToPause(); - } - } - - @Override - public void onStop() { - if (this.playButton.getVisibility() != View.VISIBLE) { - togglePauseToPlay(); - } - - if (seekBar.getProgress() + 5 >= seekBar.getMax()) { - backwardsCounter = 4; - onProgress(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 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 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 View.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 View.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 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); - } - } - } - - 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/components/AudioView.kt b/src/org/thoughtcrime/securesms/components/AudioView.kt new file mode 100644 index 0000000000..6e640662d8 --- /dev/null +++ b/src/org/thoughtcrime/securesms/components/AudioView.kt @@ -0,0 +1,267 @@ +package org.thoughtcrime.securesms.components + +import android.content.Context +import android.content.res.ColorStateList +import android.graphics.Color +import android.graphics.PorterDuff +import android.graphics.drawable.AnimatedVectorDrawable +import android.os.Build +import android.util.AttributeSet +import android.view.View +import android.view.View.OnTouchListener +import android.view.ViewGroup +import android.widget.FrameLayout +import android.widget.ImageView +import android.widget.SeekBar +import android.widget.SeekBar.OnSeekBarChangeListener +import android.widget.TextView +import androidx.core.content.ContextCompat +import androidx.core.graphics.BlendModeColorFilterCompat.createBlendModeColorFilterCompat +import androidx.core.graphics.BlendModeCompat +import com.pnikosis.materialishprogress.ProgressWheel +import network.loki.messenger.R +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 kotlin.math.floor + +class AudioView: FrameLayout, AudioSlidePlayer.Listener { + + companion object { + private const val TAG = "AudioViewKt" + } + + private val controlToggle: AnimatingToggle + private val container: ViewGroup + private val playButton: ImageView + private val pauseButton: ImageView + private val downloadButton: ImageView + private val downloadProgress: ProgressWheel + private val seekBar: SeekBar + private val timestamp: TextView + + private var downloadListener: SlideClickListener? = null + private var audioSlidePlayer: AudioSlidePlayer? = null + private var backwardsCounter = 0 + + constructor(context: Context): this(context, null) + + constructor(context: Context, attrs: AttributeSet?): this(context, attrs, 0) + + constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int): super(context, attrs, defStyleAttr) { + View.inflate(context, R.layout.audio_view, this) + container = findViewById(R.id.audio_widget_container) as ViewGroup + controlToggle = findViewById(R.id.control_toggle) as AnimatingToggle + playButton = findViewById(R.id.play) as ImageView + pauseButton = findViewById(R.id.pause) as ImageView + downloadButton = findViewById(R.id.download) as ImageView + downloadProgress = findViewById(R.id.download_progress) as ProgressWheel + seekBar = findViewById(R.id.seek) as SeekBar + timestamp = findViewById(R.id.timestamp) as TextView + + playButton.setOnClickListener { + try { + Log.d(TAG, "playbutton onClick") + if (audioSlidePlayer != null) { + togglePlayToPause() + audioSlidePlayer!!.play(getProgress()) + } + } catch (e: IOException) { + Log.w(TAG, e) + } + } + pauseButton.setOnClickListener { + Log.d(TAG, "pausebutton onClick") + if (audioSlidePlayer != null) { + togglePauseToPlay() + audioSlidePlayer!!.stop() + } + } + seekBar.setOnSeekBarChangeListener(SeekBarModifiedListener()) + + playButton.setImageDrawable(ContextCompat.getDrawable(context, R.drawable.play_icon)) + pauseButton.setImageDrawable(ContextCompat.getDrawable(context, R.drawable.pause_icon)) + playButton.background = ContextCompat.getDrawable(context, R.drawable.ic_circle_fill_white_48dp) + pauseButton.background = ContextCompat.getDrawable(context, R.drawable.ic_circle_fill_white_48dp) + + if (attrs != null) { + val typedArray = context.theme.obtainStyledAttributes(attrs, R.styleable.AudioView, 0, 0) + setTint(typedArray.getColor(R.styleable.AudioView_foregroundTintColor, Color.WHITE), + typedArray.getColor(R.styleable.AudioView_backgroundTintColor, Color.WHITE)) + container.setBackgroundColor(typedArray.getColor(R.styleable.AudioView_widgetBackground, Color.TRANSPARENT)) + typedArray.recycle() + } + } + + override fun onAttachedToWindow() { + super.onAttachedToWindow() + if (!EventBus.getDefault().isRegistered(this)) EventBus.getDefault().register(this) + } + + override fun onDetachedFromWindow() { + super.onDetachedFromWindow() + EventBus.getDefault().unregister(this) + } + + fun setAudio(audio: AudioSlide, showControls: Boolean) { + if (showControls && audio.isPendingDownload) { + controlToggle.displayQuick(downloadButton) + seekBar.isEnabled = false + downloadButton.setOnClickListener { v -> downloadListener?.onClick(v, audio) } + if (downloadProgress.isSpinning) { + downloadProgress.stopSpinning() + } + } else if (showControls && audio.transferState == AttachmentDatabase.TRANSFER_PROGRESS_STARTED) { + controlToggle.displayQuick(downloadProgress) + seekBar.isEnabled = false + downloadProgress.spin() + } else { + controlToggle.displayQuick(playButton) + seekBar.isEnabled = true + if (downloadProgress.isSpinning) { + downloadProgress.stopSpinning() + } + } + audioSlidePlayer = AudioSlidePlayer.createFor(context, audio, this) + } + + fun cleanup() { + if (audioSlidePlayer != null && pauseButton.visibility == View.VISIBLE) { + audioSlidePlayer!!.stop() + } + } + + fun setDownloadClickListener(listener: SlideClickListener?) { + downloadListener = listener + } + + fun setTint(foregroundTint: Int, backgroundTint: Int) { + playButton.backgroundTintList = ColorStateList.valueOf(foregroundTint) + playButton.imageTintList = ColorStateList.valueOf(backgroundTint) + pauseButton.backgroundTintList = ColorStateList.valueOf(foregroundTint) + pauseButton.imageTintList = ColorStateList.valueOf(backgroundTint) + + downloadButton.setColorFilter(foregroundTint, PorterDuff.Mode.SRC_IN) + downloadProgress.barColor = foregroundTint + timestamp.setTextColor(foregroundTint) + + val colorFilter = createBlendModeColorFilterCompat(foregroundTint, BlendModeCompat.SRC_IN) + seekBar.progressDrawable.colorFilter = colorFilter + seekBar.thumb.colorFilter = colorFilter + } + + override fun onStart() { + if (pauseButton.visibility != View.VISIBLE) { + togglePlayToPause() + } + } + + override fun onStop() { + if (playButton.visibility != View.VISIBLE) { + togglePauseToPlay() + } + if (seekBar.progress + 5 >= seekBar.max) { + backwardsCounter = 4 + onProgress(0.0, 0) + } + } + + override fun setFocusable(focusable: Boolean) { + super.setFocusable(focusable) + playButton.isFocusable = focusable + pauseButton.isFocusable = focusable + seekBar.isFocusable = focusable + seekBar.isFocusableInTouchMode = focusable + downloadButton.isFocusable = focusable + } + + override fun setClickable(clickable: Boolean) { + super.setClickable(clickable) + playButton.isClickable = clickable + pauseButton.isClickable = clickable + seekBar.isClickable = clickable + seekBar.setOnTouchListener(if (clickable) null else + OnTouchListener { _, _ -> return@OnTouchListener true }) // Suppress touch events. + downloadButton.isClickable = clickable + } + + override fun setEnabled(enabled: Boolean) { + super.setEnabled(enabled) + playButton.isEnabled = enabled + pauseButton.isEnabled = enabled + seekBar.isEnabled = enabled + downloadButton.isEnabled = enabled + } + + override fun onProgress(progress: Double, millis: Long) { + val seekProgress = floor(progress * seekBar.max).toInt() + if (seekProgress > seekBar.progress || backwardsCounter > 3) { + backwardsCounter = 0 + seekBar.progress = seekProgress + timestamp.text = String.format("%02d:%02d", + TimeUnit.MILLISECONDS.toMinutes(millis), + TimeUnit.MILLISECONDS.toSeconds(millis)) + } else { + backwardsCounter++ + } + } + + private fun getProgress(): Double { + return if (seekBar.progress <= 0 || seekBar.max <= 0) { + 0.0 + } else { + seekBar.progress.toDouble() / seekBar.max.toDouble() + } + } + + private fun togglePlayToPause() { + controlToggle.displayQuick(pauseButton) + val playToPauseDrawable = ContextCompat.getDrawable(context, R.drawable.play_to_pause_animation) as AnimatedVectorDrawable + pauseButton.setImageDrawable(playToPauseDrawable) + playToPauseDrawable.start() + } + + private fun togglePauseToPlay() { + controlToggle.displayQuick(playButton) + val pauseToPlayDrawable = ContextCompat.getDrawable(context, R.drawable.pause_to_play_animation) as AnimatedVectorDrawable + playButton.setImageDrawable(pauseToPlayDrawable) + pauseToPlayDrawable.start() + } + + private inner class SeekBarModifiedListener : OnSeekBarChangeListener { + override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {} + + @Synchronized + override fun onStartTrackingTouch(seekBar: SeekBar) { + if (audioSlidePlayer != null && pauseButton.visibility == View.VISIBLE) { + audioSlidePlayer!!.stop() + } + } + + @Synchronized + override fun onStopTrackingTouch(seekBar: SeekBar) { + try { + if (audioSlidePlayer != null && pauseButton.visibility == View.VISIBLE) { + audioSlidePlayer!!.play(getProgress()) + } + } catch (e: IOException) { + Log.w(TAG, e) + } + } + } + + @Subscribe(sticky = true, threadMode = ThreadMode.MAIN) + fun onEventAsync(event: PartProgressEvent) { + if (audioSlidePlayer != null && event.attachment == audioSlidePlayer!!.audioSlide.asAttachment()) { + downloadProgress.setInstantProgress(event.progress.toFloat() / event.total) + } + } +} \ No newline at end of file