Порой нам недостает функциональности приложений, которые мы используем каждый день. Имея при этом навыки программирования, хочется сделать что-нибудь свое: продукт, который будет иметь все необходимые функции, которые вам нужны. Так я решил написать свой собственный андроид-плеер, но столкнулся с серьезной трудностью — чтобы сделать более-менее пригодный плеер, требуется катастрофически много времени на программирование, а тем более на отладку. Погуглив немного на тему 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
	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 && vol_track_select) { // isproximity , vol_track_select - признаки что сработал датчик приближения 
// и активирована опция переключения треков клавишами регулировки звука

				long now = SystemClock.elapsedRealtime();
				if (now - mLastShakeTime > MIN_SHAKE_PERIOD * 8) {
					// запускаем отдельный поток, который вернет уровень громкости обратно
                                        MyTask mt;
					mt = new MyTask();
					mt.execute();

					delta = previousVolume - currentVolume;
					mLastShakeTime = 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;
					currentVolume = audio.getStreamVolume(AudioManager.STREAM_MUSIC);
				}

			}else delta=0;
		}


	}



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

class MyTask extends AsyncTask<Void, Void, Void>
class MyTask extends AsyncTask<Void, Void, Void> {
int currentVolume;
		@Override
		protected void onPreExecute() {
			super.onPreExecute();
			  currentVolume = mAudioManager.getStreamVolume(AudioManager.STREAM_MUSIC);
		}

		@Override
		protected Void doInBackground(Void... params) {
			try {
				TimeUnit.SECONDS.sleep(1);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			return null;
		}

		@Override
		protected void onPostExecute(Void result) {
			super.onPostExecute(result);
			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);

			}

		}
	}


Итак, функционал для переключения треков готов. Для того чтобы треки переключались при условии нахождения смартфона в кармане, нам нужно будет задавать переменную 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 — время отключения, считанное из настроек. Поместим этот код в обработчик собития из акселерометра, при взятии его в руки:

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 ) // если смартфон взяли в руки
 {
  // выполнить отложение засыпания
 mHandler.removeMessages(MSG_FADE_OUT); 
 mHandler.removeMessages(MSG_IDLE_TIMEOUT);
 if (mIdleTimeout != 0)
 mHandler.sendEmptyMessageDelayed(MSG_IDLE_TIMEOUT, mIdleTimeout * 1000);
 }
...
}

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

Комментарии (4)


  1. r_ii
    16.10.2015 12:19
    +1

    Я так понимаю задача MyTask — отложенное выполнение кода (изменение громкости). Если так, то это ужасное решение и антипаттерн.
    Один из вариантов правильного решения — использовать Handler.
    Почему так — ну тут лучше самому почитать больше о многопоточности.


    1. wowancoder
      16.10.2015 12:30
      -1

      Я не специалист по программировамированию на андроид, учусь в основном на примерах, спасибо за идею!


      1. andreich
        17.10.2015 00:12

        Согласен с коментарием выше. Никогда не используйте sleep для таких целей. Есть таймер или, как выше сказано, Handler.


      1. bejibx
        21.10.2015 12:58

        Согласен с r_ii за тем лишь исключением, что неплохо было бы всё таки объяснить, почему же решение является ужасным. Использовать в данном случае AsyncTask — это всё равно, что стрелять из пушки по воробьям. Данный класс предназначен для выполнения кода в другом потоке. Все методы класса, кроме doInBackground(Params...), выполняются в главном потоке приложения. В вашем AsyncTask'е, единственное, что выполняется в рабочем потоке — его остановка на 1 секунду для последующего отложенного выполнения кода в основном потоке — нерационально. Обратите внимание на класс Handler, который используется авторами приложения для отложенного завершения работы сервиса.

        There are two main uses for a Handler: (1) to schedule messages and runnables to be executed as some point in the future; and (2) to enqueue an action to be performed on a different thread than your own.
        Вам понадобятся методы postDelayed(java.lang.Runnable, long) и removeCallbacks(java.lang.Runnable).