Pull to refresh

Модифицируем плеер Vanilla Music под Android

Reading time8 min
Views12K
Порой нам недостает функциональности приложений, которые мы используем каждый день. Имея при этом навыки программирования, хочется сделать что-нибудь свое: продукт, который будет иметь все необходимые функции, которые вам нужны. Так я решил написать свой собственный андроид-плеер, но столкнулся с серьезной трудностью — чтобы сделать более-менее пригодный плеер, требуется катастрофически много времени на программирование, а тем более на отладку. Погуглив немного на тему open-source плееров для андроид, я быстро нашел проект Vanilla Music в Google Play, а затем и на Github. Скачав исходники, я вскоре вскоре принялся его модифицировать под свои нужды.

Я уже давно пытаюсь освоить программирование под Android и пишу приложения под собственные нужды, иногда выкладывая их на Google Play. В этот раз мне захотелось плеер с переключением песен по клавишам громкости. Это конечно неудобно, если необходимо поменять громкость -поэтому вторая версия идеи звучала так: переключение песен клавишами громкости должно происходить только тогда, когда телефон находится в кармане, иначе просто регулировать громкость. Второе, что мне хотелось бы иметь в функционале плеера — возможность сместить время остановки воспроизведения, если устройство активно использовалось. Итак, приступим к практике!

Открыв исходники плеера, я принялся разбираться, где и как происходит управление воспроизведением музыки. Как оказалось — это класс PlaybackService, судя по наличию функций для воспроизведения, остановки, переключения треков. Этот функционал был заложен в функцию performAction этого класса:

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);
		}
	}


Как видим, названия команд звучат весьма недвусмысленно. Выделяем нужные нам команды NextSong (следующий трек), PreviousSong (предыдущий трек) и PlayPause (установить паузу при воспроизведении или воспроизвести при паузе). Для того чтобы отловить нажатие клавиш громкости внутри сервера (это сложнее, чем внутри Activity), создадим функционал оценки изменения громкости. Для этого нам понадобится ContentObserver. Внесем класс в пространство PlaybackService:

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);
		}
}


Чтобы SettingsContentObserver заработал, необходимо связать его с Playbackservice. Для этого в функции onCreate() класса SettingsContentObserver запишем следующие строки:

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

Чтобы класс SettingsContentObserver смог определять нажатия клавиш громкости, будем считывать и анализировать на изменения громкость из объекта mAudioManager, который уже объявлен и инициализирован в классе Playbackservice.

Вот код измененного класса SettingsContentObserver:
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 использует также следующие переменные класса PlaybackService (которые необходимо объявить):
	boolean isproximity  = false;  // признак перекрытия датчика приближения
	boolean enable_vol_track_select = true; //   вкл  переключение клавишами громкости
	long mLastVolChangeTime=0;   //  время последнего изменения громкости
	int delta = 0;   //  разница в предыдущей и текущей измененной громкости

. Т.к. мы работаем с оценкой громкости, если пользователь будет переключать треки — громкость изменится, поэтому мы запустим отдельную фоновую задачу, которая установит громкость в прошлое значение, если произошло ее изменение (запуск этой задачи можно найти в блоке кода выше, он выделен комментарием):

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);

}


Итак, функционал для переключения треков готов. Для того чтобы треки переключались при условии нахождения смартфона в кармане, нам нужно будет задавать переменную isproximity=true (см. класс SettingsContentObserver, метод onChange), при оценке даных с датчика приближения. Чтобы включить его в работу, отыщем метод private void setupSensor() и модифицируем ее следующим образом, добавив слушателя для датчика приближения:

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);

}

Чтобы получать и оценивать данные от сенсоров, используем уже присутствующий метод public void onSensorChanged(SensorEvent event) и добавим в его начало следующий код:

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

В этом участке кода мы меняем глобальную переменную класса Playbackservice isproximity в соответствии с данными от датчика приближения и, таким образом, включаем или отключаем возможность переключения треков по при нахождения телефона в кармане. Если рассмотреть этот метод, то ниже можно будет увидеть считывание и обработку данных из акселерометра, а именно это нам нужно, чтобы оценить, брал ли в руки пользователь свой смартфон, для отложения остановки воспроизведения. Для определения активности я экспериментально определил порог срабатывания для акселерометра, если пользователь взял смартфон в руки, добавив следующий участок кода (он отмечен комментарием) в методе onSensorChanged:

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 ) // если смартфон взяли в руки
  {
   // выполнить отложение засыпания
  }
}

Итак, мы выбрали значение, при котором акселерометр показывает, что пользователь взял телефон в руки, теперь остается только выполнить код по отложению отключения. Т.к. отключение по времени уже реализовано в Vanilla Music, не составило труда найти строки кода, выполняющие остановку по времени: они находятся в функции public void userActionTriggered():

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

Значение mIdleTimeout — время отключения, считанное из настроек. Поместим этот код в обработчик события из акселерометра, при взятии его в руки (enable_defer_stop == true — включить опцию отложения остановки воспроизведения ):

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);
			}
...
}

Подведем итоги: мы модифицировали исходный код Vanilla Music, с этими изменениями он может переключать треки в кармане и отлаживать свое засыпание на заданное время, если пользователь брал телефон в руки, в результате получилось сделать приложение под себя с минимальными затратами по времени, в этом и есть большая польза Open Source. Код, полученный в результате модификации вы можете увидеть здесь, а также скачать исходный код проекта.
UPD: Следующая статья посвящена добавлению еще одной интересной возможности.
Tags:
Hubs:
+9
Comments5

Articles