Is there any proper way to make one thread tell another one to "wait" while it is being executed? (MediaPlayer and SeekBar combo)

Viewed 69

Ok, so I am a newbie Android and java coder. Doing it as a hobby only and learning it during the free time that I can find.

I came across SeekBar and decided to create a simple media player to experiment with it. After getting the basics going, I opened Spotify and tried to mimic its SeekBar behavior.

So here's the concept: I have one Runnable running every 200ms when the MediaPlayer is in the playing status to update the left time label and the SeekBar position.

enter image description here

Now, when you hold the Spotify SeekBar and drag it around, what happens is that the leftmost time label gets updated as the SeekBar moves, displaying the timestamp of where the SeekBar is, without the music being affected. It will only effectively change the music "position" when we release the SeekBar.

So, my way of thinking about this approach was: Create a second Runnable that gets called via a Handlerwhen the user holds the SeekBar, and release the handler when the user releases the SeekBar. Also this runnable would updatw every 50ms because the act of dragging the bar is kinda fast and to visually change the timer label as the user moves the bar around.

Now, here's the code to my MainActivity doing all this:

package com.heymilkshake.simpleaudioplayer;

import androidx.appcompat.app.AppCompatActivity;

import android.media.MediaPlayer;
import android.os.Bundle;
import android.os.Handler;
import android.view.View;
import android.widget.Button;
import android.widget.SeekBar;
import android.widget.TextView;

import java.util.Arrays;
import java.util.concurrent.TimeUnit;

public class MainActivity extends AppCompatActivity implements View.OnClickListener {

    private Button playBtn;
    private Button stopBtn;
    private Button skipForward10Btn;
    private Button skipBackward10Btn;
    private TextView displayStatus;
    private TextView songCurrentTime;
    private TextView songTimeEnd;
    private SeekBar seekBar;

    private MediaPlayer mediaPlayer;
    private Handler seekBarHandler;
    private Runnable updateSeekBar;
    private Runnable updateTimer;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        findViews();
        tagButtons();

        createPlayer();
        initialize();
    }

    private void initialize() {
        songCurrentTime.setText(formatTime(mediaPlayer.getCurrentPosition()));
        songTimeEnd.setText(formatTime(mediaPlayer.getDuration()));
        seekBar.setMax(mediaPlayer.getDuration());
        seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {

            @Override
            public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
            }

            @Override
            public void onStartTrackingTouch(SeekBar seekBar) {
                if (mediaPlayer.isPlaying()) {
                    seekBarHandler.removeCallbacks(updateSeekBar);
                }
                seekBarHandler.postDelayed(updateTimer, 0);
            }

            @Override
            public void onStopTrackingTouch(SeekBar seekBar) {
                seekBarHandler.removeCallbacks(updateTimer);
                mediaPlayer.seekTo(seekBar.getProgress());
                if (mediaPlayer.isPlaying()) {
                    seekBarHandler.postDelayed(updateSeekBar, 0);
                }
            }
        });
        seekBarHandler = new Handler();
        updateSeekBar = new Runnable() {

            @Override
            public void run() {
                updateSeekAndTime();
                seekBarHandler.postDelayed(this, 200);
            }
        };
        updateTimer = new Runnable() {

            @Override
            public void run() {
                songCurrentTime.setText(formatTime(seekBar.getProgress()));
                seekBarHandler.postDelayed(this, 50);
            }
        };
    }

    private String formatTime(int ms) {
        return String.format("%d:%02d",
                TimeUnit.MILLISECONDS.toMinutes((long) ms),
                TimeUnit.MILLISECONDS.toSeconds((long) ms) -
                TimeUnit.MINUTES.toSeconds(TimeUnit.MILLISECONDS.toMinutes((long) ms)));
    }

    private void createPlayer() {
        mediaPlayer = MediaPlayer.create(this, R.raw.come_alive);
        mediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {

            @Override
            public void onCompletion(MediaPlayer mp) {
                handleStopButton();
            }
        });
    }

    private void findViews() {
        playBtn = findViewById(R.id.button_play);
        stopBtn = findViewById(R.id.button_stop);
        skipForward10Btn = findViewById(R.id.button_skip_forward_10);
        skipBackward10Btn = findViewById(R.id.button_skip_backward_10);
        displayStatus = findViewById(R.id.display_status);
        songCurrentTime = findViewById(R.id.song_time_start);
        songTimeEnd = findViewById(R.id.song_time_end);
        seekBar = findViewById(R.id.seek_bar);
    }

    private void tagButtons() {
        int i = 0;
        for (Button button : Arrays.asList(
                playBtn,
                stopBtn,
                skipForward10Btn,
                skipBackward10Btn)) {
            button.setTag(i++);
            button.setOnClickListener(this);
        }
    }

    @Override
    public void onClick(View v) {
        int clickedTag = (int) v.getTag();
        switch (clickedTag) {
            case 0:
                handlePlayButton();
                break;
            case 1:
                handleStopButton();
                break;
            case 2:
                handleForward10Btn();
                break;
            case 3:
                handleBackwards10Btn();
                break;
            default:
        }
    }

    private void handleBackwards10Btn() {
        if (mediaPlayer.getCurrentPosition() - 10000 < 0) {
            mediaPlayer.seekTo(0);
        } else {
            mediaPlayer.seekTo(mediaPlayer.getCurrentPosition() - 10000);
        }
        updateSeekAndTime();
    }

    private void handleForward10Btn() {
        if (mediaPlayer.getCurrentPosition() + 10000 > mediaPlayer.getDuration()) {
            mediaPlayer.seekTo(mediaPlayer.getDuration());
        } else {
            mediaPlayer.seekTo(mediaPlayer.getCurrentPosition() + 10000);
        }
        updateSeekAndTime();
    }

    private void updateSeekAndTime() {
        seekBar.setProgress(mediaPlayer.getCurrentPosition());
        songCurrentTime.setText(formatTime(seekBar.getProgress()));
    }

    private void handleStopButton() {
        mediaPlayer.pause();
        mediaPlayer.seekTo(0);
        seekBarHandler.removeCallbacks(updateSeekBar);
        updateSeekAndTime();
        displayStatus.setText("Stopped");
        playBtn.setText("Play ►");
    }

    private void handlePlayButton() {
        if (mediaPlayer.isPlaying()) {
            mediaPlayer.pause();
            seekBarHandler.removeCallbacks(updateSeekBar);
            displayStatus.setText("Paused");
            playBtn.setText("Play ►");
        } else {
            mediaPlayer.start();
            seekBarHandler.postDelayed(updateSeekBar, 0);
            displayStatus.setText("Playing");
            playBtn.setText("Pause ||");
        }
    }
}

When I tested my code, I got this behavior: The bar jumping back and forth to and from the location of where the music is playing while the user is dragging it (gif for visualization of the undesired behavior)

unwanted behavior

Well, I am perfectly aware why this happens:updateSeekBar is running every 200ms, calling method updateSeekAndTime() which changes the label and bar to the current position of where the music is playing. So, when the onStartTrackingTouch detects that the user is holding the SeekBar, we run the updateTimer runnable every 50ms. This Runnable changes the text of the time label concurrently with the updateSeekBar Runnable, causing the above undesired behavior.

To solve this, I modified the code of the SeekBar.OnSeekBarChangeListener() interface anonymous implementation by adding some condition checks: If the song is playing and the user grabs the SeekBar, remove the callback to the updateSeekBar so that it stops running and when the user releases the bar, run it again. So here are the code changes:

seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {

    @Override
    public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
    }

    @Override
    public void onStartTrackingTouch(SeekBar seekBar) {
        if (mediaPlayer.isPlaying()) {
            seekBarHandler.removeCallbacks(updateSeekBar);
        }
        seekBarHandler.postDelayed(updateTimer, 0);
    }

    @Override
    public void onStopTrackingTouch(SeekBar seekBar) {
        seekBarHandler.removeCallbacks(updateTimer);
        mediaPlayer.seekTo(seekBar.getProgress());
        if (mediaPlayer.isPlaying()) {
            seekBarHandler.postDelayed(updateSeekBar, 0);
        }
    }
});

Ok, This worked just great! Look at another gif:

desired behavior

But this left me wondering and it's here that I ask you guys:

  • Is there a better approach to do this?
  • Is there any way for a Thread that is looping with the help of a Handler to instruct another Thread to "wait" while it is being executed?
  • How would YOU solve this problem??

IMPORTANT NOTE: Try to simplify or talk about stuff in a simple manner as I am just a Java and Android beginner.

Thank you all!!

0 Answers
Related