That fuck shit the fascists are using
at master 557 lines 19 kB view raw
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}