[Huawei Developers] Freemind R:
More information like this, you can visit HUAWEI Developer Forum​

Original article link: https://forums.developer.huawei.com/forumPortal/en/topicview?tid=0202327658275700021&fid=0101187876626530001

[IMG="https://communityfile-dre.op.hicloud.com/FileServer/getFile/cmtybbs/026/505/491/5190090000026505491.20200814053033.90273249776643810618296688858972:50510828024839:2800:0294F5DC5B1C6941BAC8347917566F3ED93E5347C259065F07A2321779C8050A.png"]https://communityfile-dre.op.hicloud.com/FileServer/getFile/cmtybbs/026/505/491/5190090000026505491.20200814053033.90273249776643810618296688858972:50510828024839:2800:0294F5DC5B1C6941BAC8347917566F3ED93E5347C259065F07A2321779C8050A.png[/IMG]

If you followed the part one here, you know where we left off. In this part of the tutorial, we will be implemeting onCreate in detail, including button clicks and listeners which is a crucial part in playback control.

Let’s start by implemeting add/remove listeners to control the listener attachment. Then implement a small part of onCreate method.

public void addListener(HwAudioStatusListener listener) {
if (mHwAudioManager != null) {
try {
mHwAudioManager.addPlayerStatusListener(listener);
} catch (RemoteException e) {
Log.e("TAG", "TAG", e);
}
} else {
mTempListeners.add(listener);
}
}
public void removeListener(HwAudioStatusListener listener) { //will be called in onDestroy() method
if (mHwAudioManager != null) {
try {
mHwAudioManager.removePlayerStatusListener(listener);
} catch (RemoteException e) {
Log.e("TAG", "TAG", e);
}
}
mTempListeners.remove(listener);
}
@Override
protected void onDestroy() {
if(mHwAudioPlayerManager!= null && isReallyPlaying){
isReallyPlaying = false;
mHwAudioPlayerManager.stop();
removeListener(mPlayListener);
}
super.onDestroy();
}
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = ActivityMainBinding.inflate(getLayoutInflater());
View view = binding.getRoot();
setContentView(view);
//I set the MainActivity cover image as a placeholder image to cover for those audio files which do not have a cover image.
binding.albumPictureImageView.setImageDrawable(getDrawable(R.drawable.ic_launcher_foreground));
initializeManagerAndGetPlayList(this); //I call my method to set my playlist
addListener(mPlayListener); //I add my listeners
}

If you followed the first part closely, you already have the mTempListeners variable. So, in onCreate, we first initialize everything and then attach our listener to it.

Now that our playlist is programatically ready, if you try to run your code, you will not be able to control what you want because we have not touched our views yet.

Let’s implement our basic buttons first.

final Drawable drawablePlay = getDrawable(R.drawable.btn_playback_play_normal);
final Drawable drawablePause = getDrawable(R.drawable.btn_playback_pause_normal);
binding.playButtonImageView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
if(binding.playButtonImageView.getDrawable().getConstantState().equals(drawablePlay.getConstantState())){
if (mHwAudioPlayerManager != null){
mHwAudioPlayerManager.play();
binding.playButtonImageView.setImageDrawable(getDrawable(R.drawable.btn_playback_pause_normal));
isReallyPlaying = true;
}
}
else if(binding.playButtonImageView.getDrawable().getConstantState().equals(drawablePause.getConstantState())){
if (mHwAudioPlayerManager != null) {
mHwAudioPlayerManager.pause();
binding.playButtonImageView.setImageDrawable(getDrawable(R.drawable.btn_playback_play_normal));
isReallyPlaying = false;
}
}
}
});
binding.nextSongImageView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
if (mHwAudioPlayerManager != null) {
mHwAudioPlayerManager.playNext();
isReallyPlaying = true;
}
}
});
binding.previousSongImageView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
if (mHwAudioPlayerManager != null) {
mHwAudioPlayerManager.playPre();
isReallyPlaying = true;
}
}
});

Do these in your onCreate(…) method. isReallyPlaying is a global boolean variable that I created to keep track of playback. I update it everytime the playback changes. You do not have to have it, as I also tested it with online playlists. I still keep there in case you also want to test your app with online playlists, as in the sample apps provided by Huawei.

For button visual changes, I devised a method like above and I am not asserting that it is the best. You can use your own method but onClicks should rougly be like this.

Now we should implement our listener so that we can track the change in playback. It will also include updating the UI elements, so that the app tracks the audio file changes and seekbar updates. Put this listener outside onCreate(…) method, it will be called anytime it is required. What we should do is to implement necessary methods to respond this calling.

HwAudioStatusListener mPlayListener = new HwAudioStatusListener() {
@Override
public void onSongChange(HwAudioPlayItem hwAudioPlayItem) {
setSongDetails(hwAudioPlayItem);
if(mHwAudioPlayerManager.getOffsetTime() != -1 && mHwAudioPlayerManager.getDuration() != -1)
updateSeekBar(mHwAudioPlayerManager.getOffsetTime(), mHwAudioPlayerManager.getDuration());
}
@Override
public void onQueueChanged(List list) {
if (mHwAudioPlayerManager != null && list.size() != 0 && !isReallyPlaying) {
mHwAudioPlayerManager.play();
isReallyPlaying = true;
binding.playButtonImageView.setImageDrawable(getDrawable(R.drawable.btn_playback_pause_normal));
}
}
@Override
public void onBufferProgress(int percent) {
}
@Override
public void onPlayProgress(final long currentPosition, long duration) {
updateSeekBar(currentPosition, duration);
}
@Override
public void onPlayCompleted(boolean isStopped) {
if (mHwAudioPlayerManager != null && isStopped) {
mHwAudioPlayerManager.playNext();
}
isReallyPlaying = !isStopped;
}
@Override
public void onPlayError(int errorCode, boolean isUserForcePlay) {
Toast.makeText(MainActivity.this, "We cannot play this!!", Toast.LENGTH_LONG).show();
}
@Override
public void onPlayStateChange(boolean isPlaying, boolean isBuffering) {
if(isPlaying || isBuffering){
binding.playButtonImageView.setImageDrawable(getDrawable(R.drawable.btn_playback_pause_normal));
isReallyPlaying = true;
}
else{
binding.playButtonImageView.setImageDrawable(getDrawable(R.drawable.btn_playback_play_normal));
isReallyPlaying = false;
}
}
};

Now let’s understand the snippet above. Status listener instance wants us to implement some methods shown. They are not required but they will make our lives easier. I will explain the methods inside of them later.

Understanding HwAudioStatusListener

onSongChange is called upon a song change in the queue, i.e. whenever the you skip a song for example, it will be called. Also, it is called for the first time you set the playlist because technically your song changed, from nothing to your index 0 audio file.

onQueueChanged is called upon the queue is altered. I implemented some code just in case but since we will not be dealing with adding/removing items to the playlist and change the queues, it is not very important.

onBufferProgress returns the progress as percentage when you are using online audio.

onPlayProgress is one of the most important method here, everytime the playback is changed, it is called. That is, even the audio files proceed one second (i.e. plays) it will be called. So, it is wise to call UI updates here, rather than dealing with heavy-load fragments, in my opinion.

onPlayCompleted lets you decide the behaviour when the playlist finishes playing. You can restart the playlist, just stop or do something else (like notifying the user that the playlist has ended etc.). It is totally up to you. I restart the playlist in the sample code.

onPlayError is called whenever there is an error occurred in the playback. It could, for example, be that the buffered song is not loaded correctly, format not supported, audio file is corrupted etc. You can handle the result here or you can just notify the user that the file cannot be played; just like I did.

And finally, onPlayStateChange is called whenever the music is paused/re-played. Thus, you can handle the changes here in general. I update isReallyPlaying variable and play/pause button views inside and outside of this method just to be sure. You can update it just here if you want. It should work in theory.

Extra Methods

This listener contains two extra custom methods that I wrote to update the UI and set/render the details of the song on the screen. They are called setSongDetails(…) and updateSeekBar(…). Let’s see how they are implemented.

public void updateSeekBar(final long currentPosition, long duration){
//seekbar
binding.musicSeekBar.setMax((int) (duration / 1000));
if(mHwAudioPlayerManager != null){
int mCurrentPosition = (int) (currentPosition / 1000);
binding.musicSeekBar.setProgress(mCurrentPosition);
setProgressText(mCurrentPosition);
}
binding.musicSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
//Log.i("ONSTOPTRACK", "STOP TRACK TRIGGERED.");
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
//Log.i("ONSTARTTRACK", "START TRACK TRIGGERED.");
}
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
if(mHwAudioPlayerManager != null && fromUser){
mHwAudioPlayerManager.seekTo(progress*1000);
}
if(!isReallyPlaying){
setProgressText(progress); //when the song is not playing and user updates the seekbar, seekbar still should be updated
}
}
});
}
public void setProgressText(int progress){
String progressText = String.format(Locale.US, "d:d",
TimeUnit.MILLISECONDS.toMinutes(progress*1000),
TimeUnit.MILLISECONDS.toSeconds(progress*1000) -
TimeUnit.MINUTES.toSeconds(TimeUnit.MILLISECONDS.toMinutes(progress*1000))
);
binding.progressTextView.setText(progressText);
}
public void setSongDetails(HwAudioPlayItem currentItem){
if(currentItem != null){
getBitmapOfCover(currentItem);
if(!currentItem.getAudioTitle().equals(""))
binding.songNameTextView.setText(currentItem.getAudioTitle());
else{
binding.songNameTextView.setText("Try choosing a song");
}
if(!currentItem.getSinger().equals(""))
binding.artistNameTextView.setText(currentItem.getSinger());
else
binding.artistNameTextView.setText("From the playlist");
binding.albumNameTextView.setText("Album Unknown"); //there is no field assinged to this in HwAudioPlayItem
binding.progressTextView.setText("00:00"); //initial progress of every song
long durationTotal = currentItem.getDuration();
String totalDurationText = String.format(Locale.US, "d:d",
TimeUnit.MILLISECONDS.toMinutes(durationTotal),
TimeUnit.MILLISECONDS.toSeconds(durationTotal) -
TimeUnit.MINUTES.toSeconds(TimeUnit.MILLISECONDS.toMinutes(durationTotal))
);
binding.totalDurationTextView.setText(totalDurationText);
}
else{
//Here is measure to prevent the bad first opening of the app. After user
//selects a song from the playlist, here should never be executed again.
binding.songNameTextView.setText("Try choosing a song");
binding.artistNameTextView.setText("From the playlist");
binding.albumNameTextView.setText("It is at right-top of the screen");
}
}

Remarks

I format the time everytime I receive it because currently AudioKit sends them as miliseconds and the user needs to see in 00:00 format.

I divide by 1000 and while seeking to progress, multiply by 1000 to keep the changes visible on screen. It is also more convenient and accustomed way of implementing seekbars.

I take measure by if checks in case they return null. It should not happen except the first opening but still, as developers, we should be careful.

Please follow comments in the code for additional information.

That’s it for this part. However, we are not done yet. As I mentioned earlier we should also implement the playlist to choose songs from and implement advanced playback controls "); background-size: 1px 1px; background-position: 0px calc(1em + 1px); box-sizing: inherit; font-family: medium-content-serif-font, Georgia, Cambria, "Times New Roman", Times, serif; font-weight: 700; font-size: 18px; text-decoration: underline;">in part 3. See you there!