📜 ⬆️ ⬇️

Modifying the Vanilla Music Player for Android

Sometimes we lack the functionality of the applications that we use every day. With this programming skills, I want to do something of my own: a product that will have all the necessary functions that you need. So I decided to write my own android player, but I ran into serious difficulty - to make a more or less usable player requires a lot of programming time, and even more debugging. Googling a little on the topic of open-source players for Android, I quickly found the Vanilla Music project on Google Play, and then on Github. After downloading the source code, I soon soon began to modify it to fit my needs.

I have been trying to master Android programming for a long time and write applications for my own needs, sometimes putting them on Google Play. This time I wanted the player to switch songs on the volume keys. This is of course inconvenient if you need to change the volume — therefore, the second version of the idea sounded like this: switching the songs with the volume keys should occur only when the phone is in your pocket, otherwise just adjust the volume. The second thing I would like to have in the player's functionality is the ability to shift the playback stop time if the device was actively used. So let's get down to practice!

Having opened the source of the player, I began to understand where and how the music playback is managed. As it turned out, this is a PlaybackService class, judging by the presence of functions for playing, stopping, switching tracks. This functionality was incorporated into the performAction function of this class:

public void performAction (Action action, PlaybackActivity receiver)
/** * Execute the given action. * * @param action The action to execute. * @param receiver Optional. If non-null, update the PlaybackActivity with * new song or state from the executed action. The activity will still be * updated by the broadcast if not passed here; passing it just makes the * update immediate. */ public void performAction(Action action, PlaybackActivity receiver) { switch (action) { case Nothing: break; case Library: Intent intent = new Intent(this, LibraryActivity.class); intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK); startActivity(intent); break; case PlayPause: { int state = playPause(); if (receiver != null) receiver.setState(state); break; } case NextSong: { Song song = shiftCurrentSong(SongTimeline.SHIFT_NEXT_SONG); if (receiver != null) receiver.setSong(song); break; } case PreviousSong: { Song song = shiftCurrentSong(SongTimeline.SHIFT_PREVIOUS_SONG); if (receiver != null) receiver.setSong(song); break; } case NextAlbum: { Song song = shiftCurrentSong(SongTimeline.SHIFT_NEXT_ALBUM); if (receiver != null) receiver.setSong(song); break; } case PreviousAlbum: { Song song = shiftCurrentSong(SongTimeline.SHIFT_PREVIOUS_ALBUM); if (receiver != null) receiver.setSong(song); break; } case Repeat: { int state = cycleFinishAction(); if (receiver != null) receiver.setState(state); break; } case Shuffle: { int state = cycleShuffle(); if (receiver != null) receiver.setState(state); break; } case EnqueueAlbum: enqueueFromCurrent(MediaUtils.TYPE_ALBUM); break; case EnqueueArtist: enqueueFromCurrent(MediaUtils.TYPE_ARTIST); break; case EnqueueGenre: enqueueFromCurrent(MediaUtils.TYPE_GENRE); break; case ClearQueue: clearQueue(); Toast.makeText(this, R.string.queue_cleared, Toast.LENGTH_SHORT).show(); break; case ShowQueue: Intent intentShowQueue = new Intent(this, ShowQueueActivity.class); intentShowQueue.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK); startActivity(intentShowQueue); break; case ToggleControls: // Handled in FullPlaybackActivity.performAction break; case SeekForward: if (mCurrentSong != null) { mPendingSeekSong = mCurrentSong.id; mPendingSeek = getPosition() + 10000; // We 'abuse' setCurrentSong as it will stop the playback and restart it // at the new position, taking care of the ui update setCurrentSong(0); } break; case SeekBackward: if (mCurrentSong != null) { mPendingSeekSong = mCurrentSong.id; mPendingSeek = getPosition() - 10000; if (mPendingSeek < 1) mPendingSeek = 1; // must at least be 1 setCurrentSong(0); } break; default: throw new IllegalArgumentException("Invalid action: " + action); } } 


As you can see, the names of the commands sound very clearly. Select the commands NextSong (next track), PreviousSong (previous track) and PlayPause we need (set a pause during playback or play during a pause). In order to catch the volume keys pressed inside the server (this is more difficult than inside the Activity), we will create a function for estimating the volume change. For this we need ContentObserver . We add a class to the PlaybackService space:
')
public class SettingsContentObserver extends ContentObserver
  public class SettingsContentObserver extends ContentObserver { Context context; public SettingsContentObserver(Context c, Handler handler) { super(handler); context = c; } @Override public boolean deliverSelfNotifications() { return super.deliverSelfNotifications(); } @Override public void onChange(boolean selfChange) { super.onChange(selfChange); } } 


In order for SettingsContentObserver to work, you must associate it with the Playbackservice. To do this, in the onCreate () function of the SettingsContentObserver class, we write the following lines:

  SettingsContentObserver mSettingsContentObserver = new SettingsContentObserver(this, new Handler()); getApplicationContext().getContentResolver().registerContentObserver(android.provider.Settings.System.CONTENT_URI, true, mSettingsContentObserver); 

In order for the SettingsContentObserver class to determine the volume key presses, we will read and analyze the volume changes from the mAudioManager object that has already been declared and initialized in the Playbackservice class.

Here is the code for the modified SettingsContentObserver class:
public class SettingsContentObserver extends ContentObserver
  // for a catch volume change public class SettingsContentObserver extends ContentObserver { int previousVolume; Context context; int prevdelta=0; public SettingsContentObserver(Context c, Handler handler) { super(handler); context = c; AudioManager audio = mAudioManager; previousVolume = audio.getStreamVolume(AudioManager.STREAM_MUSIC); } @Override public boolean deliverSelfNotifications() { return super.deliverSelfNotifications(); } @Override public void onChange(boolean selfChange) { super.onChange(selfChange); AudioManager audio = mAudioManager; int currentVolume = audio.getStreamVolume(AudioManager.STREAM_MUSIC); if(isproximity && enable_vol_track_select) { // isproximity , enable_vol_track_select -      //         long now = SystemClock.elapsedRealtime(); if (now - mLastVolChangeTime > MIN_SHAKE_PERIOD * 8) { //   handler ,      undo_volume_change(); delta = previousVolume - currentVolume; mLastVolChangeTime = now; if (delta > 0) { performAction(Action.NextSong, null); //     previousVolume = currentVolume; } else if (delta <= 0) { performAction(Action.PreviousSong, null); //     previousVolume = currentVolume; } prevdelta = delta; } else { int difvol = currentVolume - audio.getStreamVolume(AudioManager.STREAM_MUSIC); previousVolume = difvol + currentVolume; } }else delta=0; } } 


SettingsContentObserver also uses the following variables of the PlaybackService class (which must be declared):
  boolean isproximity = false; //     boolean enable_vol_track_select = true; //     long mLastVolChangeTime=0; //     int delta = 0; //        

. Since We work with volume estimation, if the user switches the tracks - the volume will change, so we will run a separate background task that will set the volume to the past value if it has changed (this task can be found in the code block above, it is highlighted with a comment):

void undo_volume_change ()
 void undo_volume_change() { // undo volume change afer next-prev comand android.os.Handler h = new Handler() { int currentVolume; public void handleMessage(android.os.Message msg) { switch (msg.what) { case 1: currentVolume = mAudioManager.getStreamVolume(AudioManager.STREAM_MUSIC); break; case 2: if(isproximity) { if(mMediaPlayer.isPlaying() ) { if(Math.abs(delta)==1) { mAudioManager.setStreamVolume(AudioManager.STREAM_MUSIC, currentVolume + delta, AudioManager.FLAG_SHOW_UI); Log.d("PLEER", " setStreamVolume Volume=" + String.valueOf(currentVolume)); } } } else { delta=0; currentVolume = mAudioManager.getStreamVolume(AudioManager.STREAM_MUSIC); } break; } }; }; h.sendEmptyMessage(1); h.sendEmptyMessageDelayed(2,1000); } 


So, the functionality for switching tracks is ready. In order for the tracks to switch when the smartphone is in your pocket, we will need to set the variable isproximity = true (see the SettingsContentObserver class, onChange method), when evaluating data from the proximity sensor. To enable it to work, we will find the private void setupSensor () method and modify it as follows, adding a listener for the proximity sensor:

 private void setupSensor() { if (mSensorManager == null) mSensorManager = (SensorManager) getSystemService(SENSOR_SERVICE); mSensorManager.registerListener(this, mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER),SensorManager.SENSOR_DELAY_GAME); mSensorManager.registerListener(this, mSensorManager.getDefaultSensor(Sensor.TYPE_PROXIMITY),SensorManager.SENSOR_DELAY_NORMAL); } 

To receive and evaluate data from sensors, use the already present method public void onSensorChanged (SensorEvent event) and add the following code to its beginning:

 if (event.sensor.getType() == Sensor.TYPE_PROXIMITY) { if( event.values[0] == 0)isproximity = true; else isproximity = false; } 

In this section of the code, we change the global class variable Playbackservice isproximity in accordance with the data from the proximity sensor and, thus, enable or disable the ability to switch tracks on when the phone is in your pocket. If we consider this method, then below we will see the reading and processing of data from the accelerometer, and this is what we need to assess whether the user took the smartphone in his hands, for deferring stopping playback. To determine the activity, I experimentally determined the trigger threshold for the accelerometer, if the user took the smartphone in his hands, adding the following piece of code (it is marked with a comment) in the onSensorChanged method:

 if (event.sensor.getType() == Sensor.TYPE_ACCELEROMETER) //      { { double x = event.values[0]; double y = event.values[1]; double z = event.values[2]; double accel = Math.sqrt(x * x + y * y + z * z); double delta = accel - mAccelLast; mAccelLast = accel; double filtered = mAccelFiltered * 0.9f + delta; mAccelFiltered = filtered; //    Log.d("PLEER", " filtered = " + String.valueOf(filtered)); if (filtered > 3.5 ) //      { //    } } 

So, we chose the value at which the accelerometer shows that the user took the phone in his hands, now it remains only to execute the code for the shutdown delay. Since the timeout has already been implemented in Vanilla Music, it was not difficult to find the lines of code that perform the timeout: they are in the public void userActionTriggered () function:

 mHandler.removeMessages(MSG_FADE_OUT); mHandler.removeMessages(MSG_IDLE_TIMEOUT); if (mIdleTimeout != 0) mHandler.sendEmptyMessageDelayed(MSG_IDLE_TIMEOUT, mIdleTimeout * 1000); 

The mIdleTimeout value is the off time read from the settings. Put this code in the event handler from the accelerometer, while taking it in your hands (enable_defer_stop == true - enable the option to delay playback stop):

 if (se.sensor.getType() == Sensor.TYPE_ACCELEROMETER) { double x = se.values[0]; double y = se.values[1]; double z = se.values[2]; double accel = Math.sqrt(x * x + y * y + z * z); double delta = accel - mAccelLast; mAccelLast = accel; double filtered = mAccelFiltered * 0.9f + delta; mAccelFiltered = filtered; if (filtered > mShakeThreshold && mShakeAction !=Action.Nothing); { long now = SystemClock.elapsedRealtime(); if (now - mLastShakeTime > MIN_SHAKE_PERIOD) { mLastShakeTime = now; performAction(mShakeAction, null); } } if (filtered > 3.5 && enable_defer_stop) //  c { mHandler.removeMessages(MSG_FADE_OUT); mHandler.removeMessages(MSG_IDLE_TIMEOUT); if (mIdleTimeout != 0) mHandler.sendEmptyMessageDelayed(MSG_IDLE_TIMEOUT, mIdleTimeout * 1000); } ... } 

To summarize: we modified the source code of Vanilla Music , with these changes it can switch tracks in a pocket and debug its sleep for a specified time, if the user took the phone in hand, the result was to make the application for themselves with minimal time costs, in this and there is a big benefit of open source. The code obtained as a result of the modification you can see here , as well as download the source code of the project .
UPD: The next article is about adding another interesting feature.

Source: https://habr.com/ru/post/268923/


All Articles