That fuck shit the fascists are using
1package org.tm.archive.components;
2
3import android.content.Context;
4import android.content.res.TypedArray;
5import android.graphics.Color;
6import android.graphics.PorterDuff;
7import android.graphics.Rect;
8import android.net.Uri;
9import android.util.AttributeSet;
10import android.view.GestureDetector;
11import android.view.MotionEvent;
12import android.view.View;
13import android.widget.FrameLayout;
14import android.widget.ImageView;
15import android.widget.SeekBar;
16import android.widget.TextView;
17
18import androidx.annotation.ColorInt;
19import androidx.annotation.NonNull;
20import androidx.annotation.Nullable;
21import androidx.core.graphics.drawable.DrawableCompat;
22import androidx.lifecycle.Observer;
23
24import com.airbnb.lottie.LottieAnimationView;
25import com.airbnb.lottie.LottieProperty;
26import com.airbnb.lottie.SimpleColorFilter;
27import com.airbnb.lottie.model.KeyPath;
28import com.airbnb.lottie.value.LottieValueCallback;
29import com.pnikosis.materialishprogress.ProgressWheel;
30
31import org.greenrobot.eventbus.EventBus;
32import org.greenrobot.eventbus.Subscribe;
33import org.greenrobot.eventbus.ThreadMode;
34import org.signal.core.util.logging.Log;
35import org.tm.archive.R;
36import org.tm.archive.audio.AudioWaveForms;
37import org.tm.archive.components.voice.VoiceNotePlaybackState;
38import org.tm.archive.database.AttachmentTable;
39import org.tm.archive.events.PartProgressEvent;
40import org.tm.archive.mms.AudioSlide;
41import org.tm.archive.mms.SlideClickListener;
42
43import java.util.Objects;
44import java.util.concurrent.TimeUnit;
45
46import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
47import io.reactivex.rxjava3.disposables.Disposable;
48
49public final class AudioView extends FrameLayout {
50
51 private static final String TAG = Log.tag(AudioView.class);
52
53 private static final int MODE_NORMAL = 0;
54 private static final int MODE_SMALL = 1;
55 private static final int MODE_DRAFT = 2;
56
57 private static final int FORWARDS = 1;
58 private static final int REVERSE = -1;
59
60 @NonNull private final AnimatingToggle controlToggle;
61 @NonNull private final View progressAndPlay;
62 @NonNull private final LottieAnimationView playPauseButton;
63 @NonNull private final ImageView downloadButton;
64 @Nullable private final ProgressWheel circleProgress;
65 @NonNull private final SeekBar seekBar;
66 private final boolean smallView;
67 private final boolean autoRewind;
68
69 @Nullable private final TextView duration;
70
71 @ColorInt private final int waveFormPlayedBarsColor;
72 @ColorInt private final int waveFormUnplayedBarsColor;
73 @ColorInt private final int waveFormThumbTint;
74
75 @Nullable private SlideClickListener downloadListener;
76 private int backwardsCounter;
77 private int lottieDirection;
78 private boolean isPlaying;
79 private long durationMillis;
80 private AudioSlide audioSlide;
81 private Callbacks callbacks;
82
83 private Disposable disposable = Disposable.disposed();
84
85 private final Observer<VoiceNotePlaybackState> playbackStateObserver = this::onPlaybackState;
86
87 public AudioView(Context context) {
88 this(context, null);
89 }
90
91 public AudioView(Context context, AttributeSet attrs) {
92 this(context, attrs, 0);
93 }
94
95 public AudioView(Context context, AttributeSet attrs, int defStyleAttr) {
96 super(context, attrs, defStyleAttr);
97 setLayoutDirection(LAYOUT_DIRECTION_LTR);
98
99 TypedArray typedArray = null;
100 try {
101 typedArray = context.getTheme().obtainStyledAttributes(attrs, R.styleable.AudioView, 0, 0);
102
103 int mode = typedArray.getInteger(R.styleable.AudioView_audioView_mode, MODE_NORMAL);
104 smallView = mode == MODE_SMALL;
105 autoRewind = typedArray.getBoolean(R.styleable.AudioView_autoRewind, false);
106
107 switch (mode) {
108 case MODE_NORMAL:
109 inflate(context, R.layout.audio_view, this);
110 break;
111 case MODE_SMALL:
112 inflate(context, R.layout.audio_view_small, this);
113 break;
114 case MODE_DRAFT:
115 inflate(context, R.layout.audio_view_draft, this);
116 break;
117 default:
118 throw new IllegalStateException("Unsupported mode: " + mode);
119 }
120
121 this.controlToggle = findViewById(R.id.control_toggle);
122 this.playPauseButton = findViewById(R.id.play);
123 this.progressAndPlay = findViewById(R.id.progress_and_play);
124 this.downloadButton = findViewById(R.id.download);
125 this.circleProgress = findViewById(R.id.circle_progress);
126 this.seekBar = findViewById(R.id.seek);
127 this.duration = findViewById(R.id.duration);
128
129 lottieDirection = REVERSE;
130 this.playPauseButton.setOnClickListener(new PlayPauseClickedListener());
131 this.playPauseButton.setOnLongClickListener(v -> performLongClick());
132 this.seekBar.setOnSeekBarChangeListener(new SeekBarModifiedListener());
133
134 setTint(typedArray.getColor(R.styleable.AudioView_foregroundTintColor, Color.WHITE));
135
136 int backgroundTintColor = typedArray.getColor(R.styleable.AudioView_backgroundTintColor, Color.TRANSPARENT);
137 if (getBackground() != null && backgroundTintColor != Color.TRANSPARENT) {
138 DrawableCompat.setTint(getBackground(), backgroundTintColor);
139 }
140
141 this.waveFormPlayedBarsColor = typedArray.getColor(R.styleable.AudioView_waveformPlayedBarsColor, Color.WHITE);
142 this.waveFormUnplayedBarsColor = typedArray.getColor(R.styleable.AudioView_waveformUnplayedBarsColor, Color.WHITE);
143 this.waveFormThumbTint = typedArray.getColor(R.styleable.AudioView_waveformThumbTint, Color.WHITE);
144
145 setProgressAndPlayBackgroundTint(typedArray.getColor(R.styleable.AudioView_progressAndPlayTint, Color.BLACK));
146 } finally {
147 if (typedArray != null) {
148 typedArray.recycle();
149 }
150 }
151 }
152
153 @Override
154 protected void onAttachedToWindow() {
155 super.onAttachedToWindow();
156 if (!EventBus.getDefault().isRegistered(this)) EventBus.getDefault().register(this);
157 }
158
159 @Override
160 protected void onDetachedFromWindow() {
161 super.onDetachedFromWindow();
162 EventBus.getDefault().unregister(this);
163 disposable.dispose();
164 }
165
166 public void setProgressAndPlayBackgroundTint(@ColorInt int color) {
167 progressAndPlay.getBackground().setColorFilter(color, PorterDuff.Mode.SRC_IN);
168 }
169
170 public Observer<VoiceNotePlaybackState> getPlaybackStateObserver() {
171 return playbackStateObserver;
172 }
173
174 public void setAudio(final @NonNull AudioSlide audio,
175 final @Nullable Callbacks callbacks,
176 final boolean showControls,
177 final boolean forceHideDuration)
178 {
179 this.callbacks = callbacks;
180
181 if (duration != null) {
182 duration.setVisibility(View.VISIBLE);
183 }
184
185 if (seekBar instanceof WaveFormSeekBarView) {
186 if (audioSlide != null && !Objects.equals(audioSlide.getUri(), audio.getUri())) {
187 WaveFormSeekBarView waveFormView = (WaveFormSeekBarView) seekBar;
188 waveFormView.setWaveMode(false);
189 seekBar.setProgress(0);
190 durationMillis = 0;
191 }
192 }
193
194 if (showControls && audio.isPendingDownload()) {
195 controlToggle.displayQuick(downloadButton);
196 seekBar.setEnabled(false);
197 downloadButton.setOnClickListener(new DownloadClickedListener(audio));
198 if (circleProgress != null) {
199 if (circleProgress.isSpinning()) circleProgress.stopSpinning();
200 circleProgress.setVisibility(View.GONE);
201 }
202 } else if (showControls && audio.getTransferState() == AttachmentTable.TRANSFER_PROGRESS_STARTED) {
203 controlToggle.displayQuick(progressAndPlay);
204 seekBar.setEnabled(false);
205 if (circleProgress != null) {
206 circleProgress.setVisibility(View.VISIBLE);
207 circleProgress.spin();
208 }
209 } else {
210 seekBar.setEnabled(true);
211 if (circleProgress != null && circleProgress.isSpinning()) circleProgress.stopSpinning();
212 showPlayButton();
213 }
214
215 if (seekBar instanceof WaveFormSeekBarView) {
216 WaveFormSeekBarView waveFormView = (WaveFormSeekBarView) seekBar;
217 waveFormView.setColors(waveFormPlayedBarsColor, waveFormUnplayedBarsColor, waveFormThumbTint);
218 if (android.os.Build.VERSION.SDK_INT >= 23) {
219 if (audioSlide == null || !Objects.equals(audioSlide.getUri(), audio.getUri())) {
220 disposable.dispose();
221 disposable = AudioWaveForms.getWaveForm(getContext(), audio.asAttachment())
222 .observeOn(AndroidSchedulers.mainThread())
223 .subscribe(
224 data -> {
225 durationMillis = data.getDuration(TimeUnit.MILLISECONDS);
226 updateProgress(0, 0);
227 if (!forceHideDuration && duration != null) {
228 duration.setVisibility(VISIBLE);
229 }
230 waveFormView.setWaveData(data.getWaveForm());
231 },
232 t -> waveFormView.setWaveMode(false)
233 );
234 }
235 } else {
236 waveFormView.setWaveMode(false);
237 if (duration != null) {
238 duration.setVisibility(GONE);
239 }
240 }
241 }
242
243 if (forceHideDuration && duration != null) {
244 duration.setVisibility(View.GONE);
245 }
246
247 this.audioSlide = audio;
248 }
249
250 public void setDownloadClickListener(@Nullable SlideClickListener listener) {
251 this.downloadListener = listener;
252 }
253
254 public @Nullable Uri getAudioSlideUri() {
255 if (audioSlide != null) return audioSlide.getUri();
256 else return null;
257 }
258
259 private void onPlaybackState(@NonNull VoiceNotePlaybackState voiceNotePlaybackState) {
260 onDuration(voiceNotePlaybackState.getUri(), voiceNotePlaybackState.getTrackDuration());
261 onProgress(voiceNotePlaybackState.getUri(),
262 (double) voiceNotePlaybackState.getPlayheadPositionMillis() / voiceNotePlaybackState.getTrackDuration(),
263 voiceNotePlaybackState.getPlayheadPositionMillis());
264 onSpeedChanged(voiceNotePlaybackState.getUri(), voiceNotePlaybackState.getSpeed());
265 onStart(voiceNotePlaybackState.getUri(), voiceNotePlaybackState.isPlaying(), voiceNotePlaybackState.isAutoReset());
266 }
267
268 private void onDuration(@NonNull Uri uri, long durationMillis) {
269 if (isTarget(uri)) {
270 this.durationMillis = durationMillis;
271 }
272 }
273
274 private void onStart(@NonNull Uri uri, boolean statePlaying, boolean autoReset) {
275 if (!isTarget(uri) || !statePlaying) {
276 if (hasAudioUri()) {
277 onStop(audioSlide.getUri(), autoReset);
278 }
279
280 return;
281 }
282
283 if (isPlaying) {
284 return;
285 }
286
287 isPlaying = true;
288 togglePlayToPause();
289 }
290
291 private void onStop(@NonNull Uri uri, boolean autoReset) {
292 if (!isTarget(uri)) {
293 return;
294 }
295
296 if (!isPlaying) {
297 return;
298 }
299
300 isPlaying = false;
301 togglePauseToPlay();
302
303 if (autoReset || autoRewind || seekBar.getProgress() + 5 >= seekBar.getMax()) {
304 backwardsCounter = 4;
305 rewind();
306 }
307 }
308
309 private void onProgress(@NonNull Uri uri, double progress, long millis) {
310 if (!isTarget(uri)) {
311 return;
312 }
313
314 int seekProgress = (int) Math.floor(progress * seekBar.getMax());
315
316 if (seekProgress > seekBar.getProgress() || backwardsCounter > 3) {
317 backwardsCounter = 0;
318 seekBar.setProgress(seekProgress);
319 updateProgress((float) progress, millis);
320 } else {
321 backwardsCounter++;
322 }
323 }
324
325 private void onSpeedChanged(@NonNull Uri uri, float speed) {
326 if (callbacks != null) {
327 callbacks.onSpeedChanged(speed, isTarget(uri));
328 }
329 }
330
331 private boolean isTarget(@NonNull Uri uri) {
332 return hasAudioUri() && Objects.equals(uri, audioSlide.getUri());
333 }
334
335 private boolean hasAudioUri() {
336 return audioSlide != null && audioSlide.getUri() != null;
337 }
338
339 @Override
340 public void setFocusable(boolean focusable) {
341 super.setFocusable(focusable);
342 this.playPauseButton.setFocusable(focusable);
343 this.seekBar.setFocusable(focusable);
344 this.seekBar.setFocusableInTouchMode(focusable);
345 this.downloadButton.setFocusable(focusable);
346 }
347
348 @Override
349 public void setClickable(boolean clickable) {
350 super.setClickable(clickable);
351 this.playPauseButton.setClickable(clickable);
352 this.seekBar.setClickable(clickable);
353 this.seekBar.setOnTouchListener(clickable ? new LongTapAwareTouchListener() : new TouchIgnoringListener());
354 this.downloadButton.setClickable(clickable);
355 }
356
357 @Override
358 public void setEnabled(boolean enabled) {
359 super.setEnabled(enabled);
360 this.playPauseButton.setEnabled(enabled);
361 this.seekBar.setEnabled(enabled);
362 this.downloadButton.setEnabled(enabled);
363 }
364
365 private void updateProgress(float progress, long millis) {
366 if (callbacks != null) {
367 callbacks.onProgressUpdated(durationMillis, millis);
368 }
369
370 if (duration != null && durationMillis > 0) {
371 long remainingSecs = Math.max(0, TimeUnit.MILLISECONDS.toSeconds(durationMillis - millis));
372 duration.setText(getResources().getString(R.string.AudioView_duration, remainingSecs / 60, remainingSecs % 60));
373 }
374
375 if (smallView && circleProgress != null) {
376 circleProgress.setInstantProgress(seekBar.getProgress() == 0 ? 1 : progress);
377 }
378 }
379
380 public void setTint(int foregroundTint) {
381 post(()-> this.playPauseButton.addValueCallback(new KeyPath("**"),
382 LottieProperty.COLOR_FILTER,
383 new LottieValueCallback<>(new SimpleColorFilter(foregroundTint))));
384
385 this.downloadButton.setColorFilter(foregroundTint, PorterDuff.Mode.SRC_IN);
386
387 if (circleProgress != null) {
388 this.circleProgress.setBarColor(foregroundTint);
389 }
390
391 if (this.duration != null) {
392 this.duration.setTextColor(foregroundTint);
393 }
394 this.seekBar.getProgressDrawable().setColorFilter(foregroundTint, PorterDuff.Mode.SRC_IN);
395 this.seekBar.getThumb().setColorFilter(foregroundTint, PorterDuff.Mode.SRC_IN);
396 }
397
398 public void getSeekBarGlobalVisibleRect(@NonNull Rect rect) {
399 seekBar.getGlobalVisibleRect(rect);
400 }
401
402 private double getProgress() {
403 if (this.seekBar.getProgress() <= 0 || this.seekBar.getMax() <= 0) {
404 return 0;
405 } else {
406 return (double)this.seekBar.getProgress() / (double)this.seekBar.getMax();
407 }
408 }
409
410 private void togglePlayToPause() {
411 startLottieAnimation(FORWARDS);
412 }
413
414 private void togglePauseToPlay() {
415 startLottieAnimation(REVERSE);
416 }
417
418 private void startLottieAnimation(int direction) {
419 showPlayButton();
420
421 if (lottieDirection == direction) {
422 return;
423 }
424 lottieDirection = direction;
425
426 playPauseButton.pauseAnimation();
427 playPauseButton.setSpeed(direction * 2);
428 playPauseButton.resumeAnimation();
429 }
430
431 private void showPlayButton() {
432 if (circleProgress != null) {
433 if (!smallView) {
434 circleProgress.setVisibility(GONE);
435 } else if (seekBar.getProgress() == 0) {
436 circleProgress.setInstantProgress(1);
437 }
438 }
439
440 playPauseButton.setVisibility(VISIBLE);
441 controlToggle.displayQuick(progressAndPlay);
442 }
443
444 public void stopPlaybackAndReset() {
445 if (audioSlide == null || audioSlide.getUri() == null) return;
446
447 if (callbacks != null) {
448 callbacks.onStopAndReset(audioSlide.getUri());
449 rewind();
450 }
451 }
452
453 private class PlayPauseClickedListener implements View.OnClickListener {
454
455 @Override
456 public void onClick(View v) {
457 if (audioSlide == null || audioSlide.getUri() == null) return;
458
459 if (callbacks != null) {
460 if (lottieDirection == REVERSE) {
461 callbacks.onPlay(audioSlide.getUri(), getProgress());
462 } else {
463 callbacks.onPause(audioSlide.getUri());
464 }
465 }
466 }
467 }
468
469 private void rewind() {
470 seekBar.setProgress(0);
471 updateProgress(0, 0);
472 }
473
474 private class DownloadClickedListener implements View.OnClickListener {
475 private final @NonNull AudioSlide slide;
476
477 private DownloadClickedListener(@NonNull AudioSlide slide) {
478 this.slide = slide;
479 }
480
481 @Override
482 public void onClick(View v) {
483 if (downloadListener != null) downloadListener.onClick(v, slide);
484 }
485 }
486
487 private class SeekBarModifiedListener implements SeekBar.OnSeekBarChangeListener {
488
489 private boolean wasPlaying;
490
491 @Override
492 public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
493 }
494
495 @Override
496 public synchronized void onStartTrackingTouch(SeekBar seekBar) {
497 if (audioSlide == null || audioSlide.getUri() == null) return;
498
499 wasPlaying = isPlaying;
500 if (isPlaying) {
501 if (callbacks != null) {
502 callbacks.onPause(audioSlide.getUri());
503 }
504 }
505 }
506
507 @Override
508 public synchronized void onStopTrackingTouch(SeekBar seekBar) {
509 if (audioSlide == null || audioSlide.getUri() == null) return;
510
511 if (callbacks != null) {
512 if (wasPlaying) {
513 callbacks.onSeekTo(audioSlide.getUri(), getProgress());
514 } else {
515 callbacks.onProgressUpdated(durationMillis, Math.round(durationMillis * getProgress()));
516 }
517 }
518 }
519 }
520
521 private class LongTapAwareTouchListener implements OnTouchListener {
522 private final GestureDetector gestureDetector = new GestureDetector(AudioView.this.getContext(), new GestureDetector.SimpleOnGestureListener() {
523 @Override
524 public void onLongPress(MotionEvent e) {
525 performLongClick();
526 }
527 });
528
529 @Override
530 public boolean onTouch(View v, MotionEvent event) {
531 return gestureDetector.onTouchEvent(event);
532 }
533 }
534
535 private static class TouchIgnoringListener implements OnTouchListener {
536 @Override
537 public boolean onTouch(View v, MotionEvent event) {
538 return true;
539 }
540 }
541
542 @Subscribe(sticky = true, threadMode = ThreadMode.MAIN)
543 public void onEventAsync(final PartProgressEvent event) {
544 if (audioSlide != null && circleProgress != null && event.attachment.equals(audioSlide.asAttachment())) {
545 circleProgress.setInstantProgress(((float) event.progress) / event.total);
546 }
547 }
548
549 public interface Callbacks {
550 void onPlay(@NonNull Uri audioUri, double progress);
551 void onPause(@NonNull Uri audioUri);
552 void onSeekTo(@NonNull Uri audioUri, double progress);
553 void onStopAndReset(@NonNull Uri audioUri);
554 void onSpeedChanged(float speed, boolean isPlaying);
555 void onProgressUpdated(long durationMillis, long playheadMillis);
556 }
557}