loading...

Make your music player with HMS Audio Kit: Part 2

singlebubble1 profile image singlebubble ・6 min read

Alt Text
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!

Posted on by:

singlebubble1 profile

singlebubble

@singlebubble1

live, laough and love coding, cooking and skiing

Discussion

markdown guide