<commit_before>package de.danoeh.antennapod.service;

import java.io.IOException;
import java.util.Date;
import java.util.concurrent.Future;
import java.util.concurrent.RejectedExecutionHandler;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

import android.annotation.SuppressLint;
import android.app.Notification;
import android.app.PendingIntent;
import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.media.AudioManager;
import android.media.AudioManager.OnAudioFocusChangeListener;
import android.media.MediaMetadataRetriever;
import android.media.MediaPlayer;
import android.media.RemoteControlClient;
import android.media.RemoteControlClient.MetadataEditor;
import android.os.AsyncTask;
import android.os.Binder;
import android.os.IBinder;
import android.preference.PreferenceManager;
import android.support.v4.app.NotificationCompat;
import android.util.Log;
import android.view.KeyEvent;
import android.view.SurfaceHolder;
import de.danoeh.antennapod.AppConfig;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.activity.AudioplayerActivity;
import de.danoeh.antennapod.activity.VideoplayerActivity;
import de.danoeh.antennapod.feed.Chapter;
import de.danoeh.antennapod.feed.FeedComponent;
import de.danoeh.antennapod.feed.FeedItem;
import de.danoeh.antennapod.feed.FeedManager;
import de.danoeh.antennapod.feed.FeedMedia;
import de.danoeh.antennapod.feed.MediaType;
import de.danoeh.antennapod.preferences.PlaybackPreferences;
import de.danoeh.antennapod.preferences.UserPreferences;
import de.danoeh.antennapod.receiver.MediaButtonReceiver;
import de.danoeh.antennapod.receiver.PlayerWidget;
import de.danoeh.antennapod.util.BitmapDecoder;
import de.danoeh.antennapod.util.flattr.FlattrUtils;
import de.danoeh.antennapod.util.playback.Playable;
import de.danoeh.antennapod.util.playback.Playable.PlayableException;

/** Controls the MediaPlayer that plays a FeedMedia-file */
public class PlaybackService extends Service {
	/** Logging tag */
	private static final String TAG = "PlaybackService";

	/** Parcelable of type Playable. */
	public static final String EXTRA_PLAYABLE = "PlaybackService.PlayableExtra";
	/** True if media should be streamed. */
	public static final String EXTRA_SHOULD_STREAM = "extra.de.danoeh.antennapod.service.shouldStream";
	/**
	 * True if playback should be started immediately after media has been
	 * prepared.
	 */
	public static final String EXTRA_START_WHEN_PREPARED = "extra.de.danoeh.antennapod.service.startWhenPrepared";

	public static final String EXTRA_PREPARE_IMMEDIATELY = "extra.de.danoeh.antennapod.service.prepareImmediately";

	public static final String ACTION_PLAYER_STATUS_CHANGED = "action.de.danoeh.antennapod.service.playerStatusChanged";
	private static final String AVRCP_ACTION_PLAYER_STATUS_CHANGED = "com.android.music.playstatechanged";

	public static final String ACTION_PLAYER_NOTIFICATION = "action.de.danoeh.antennapod.service.playerNotification";
	public static final String EXTRA_NOTIFICATION_CODE = "extra.de.danoeh.antennapod.service.notificationCode";
	public static final String EXTRA_NOTIFICATION_TYPE = "extra.de.danoeh.antennapod.service.notificationType";

	/**
	 * If the PlaybackService receives this action, it will stop playback and
	 * try to shutdown.
	 */
	public static final String ACTION_SHUTDOWN_PLAYBACK_SERVICE = "action.de.danoeh.antennapod.service.actionShutdownPlaybackService";

	/**
	 * If the PlaybackService receives this action, it will end playback of the
	 * current episode and load the next episode if there is one available.
	 * */
	public static final String ACTION_SKIP_CURRENT_EPISODE = "action.de.danoeh.antennapod.service.skipCurrentEpisode";

	/** Used in NOTIFICATION_TYPE_RELOAD. */
	public static final int EXTRA_CODE_AUDIO = 1;
	public static final int EXTRA_CODE_VIDEO = 2;

	public static final int NOTIFICATION_TYPE_ERROR = 0;
	public static final int NOTIFICATION_TYPE_INFO = 1;
	public static final int NOTIFICATION_TYPE_BUFFER_UPDATE = 2;
	public static final int NOTIFICATION_TYPE_RELOAD = 3;
	/** The state of the sleeptimer changed. */
	public static final int NOTIFICATION_TYPE_SLEEPTIMER_UPDATE = 4;
	public static final int NOTIFICATION_TYPE_BUFFER_START = 5;
	public static final int NOTIFICATION_TYPE_BUFFER_END = 6;
	/** No more episodes are going to be played. */
	public static final int NOTIFICATION_TYPE_PLAYBACK_END = 7;

	/**
	 * Returned by getPositionSafe() or getDurationSafe() if the playbackService
	 * is in an invalid state.
	 */
	public static final int INVALID_TIME = -1;

	/** Is true if service is running. */
	public static boolean isRunning = false;

	private static final int NOTIFICATION_ID = 1;

	private AudioManager audioManager;
	private ComponentName mediaButtonReceiver;

	private MediaPlayer player;
	private RemoteControlClient remoteControlClient;

	private Playable media;

	/** True if media should be streamed (Extracted from Intent Extra) . */
	private boolean shouldStream;

	/** True if service should prepare playback after it has been initialized */
	private boolean prepareImmediately;
	private boolean startWhenPrepared;
	private FeedManager manager;
	private PlayerStatus status;

	private PositionSaver positionSaver;
	private ScheduledFuture positionSaverFuture;

	private WidgetUpdateWorker widgetUpdater;
	private ScheduledFuture widgetUpdaterFuture;

	private SleepTimer sleepTimer;
	private Future sleepTimerFuture;

	private static final int SCHED_EX_POOL_SIZE = 3;
	private ScheduledThreadPoolExecutor schedExecutor;

	private volatile PlayerStatus statusBeforeSeek;

	private static boolean playingVideo;

	/** True if mediaplayer was paused because it lost audio focus temporarily */
	private boolean pausedBecauseOfTransientAudiofocusLoss;

	private Thread chapterLoader;

	private final IBinder mBinder = new LocalBinder();

	public class LocalBinder extends Binder {
		public PlaybackService getService() {
			return PlaybackService.this;
		}
	}

	@Override
	public boolean onUnbind(Intent intent) {
		if (AppConfig.DEBUG)
			Log.d(TAG, "Received onUnbind event");
		return super.onUnbind(intent);
	}

	/**
	 * Returns an intent which starts an audio- or videoplayer, depending on the
	 * type of media that is being played. If the playbackservice is not
	 * running, the type of the last played media will be looked up.
	 * */
	public static Intent getPlayerActivityIntent(Context context) {
		if (isRunning) {
			if (playingVideo) {
				return new Intent(context, VideoplayerActivity.class);
			} else {
				return new Intent(context, AudioplayerActivity.class);
			}
		} else {
			if (PlaybackPreferences.getCurrentEpisodeIsVideo()) {
				return new Intent(context, VideoplayerActivity.class);
			} else {
				return new Intent(context, AudioplayerActivity.class);
			}
		}
	}

	/**
	 * Same as getPlayerActivityIntent(context), but here the type of activity
	 * depends on the FeedMedia that is provided as an argument.
	 */
	public static Intent getPlayerActivityIntent(Context context, Playable media) {
		MediaType mt = media.getMediaType();
		if (mt == MediaType.VIDEO) {
			return new Intent(context, VideoplayerActivity.class);
		} else {
			return new Intent(context, AudioplayerActivity.class);
		}
	}

	@SuppressLint("NewApi")
	@Override
	public void onCreate() {
		super.onCreate();
		if (AppConfig.DEBUG)
			Log.d(TAG, "Service created.");
		isRunning = true;
		pausedBecauseOfTransientAudiofocusLoss = false;
		status = PlayerStatus.STOPPED;
		audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
		manager = FeedManager.getInstance();
		schedExecutor = new ScheduledThreadPoolExecutor(SCHED_EX_POOL_SIZE,
				new ThreadFactory() {

					@Override
					public Thread newThread(Runnable r) {
						Thread t = new Thread(r);
						t.setPriority(Thread.MIN_PRIORITY);
						return t;
					}
				}, new RejectedExecutionHandler() {

					@Override
					public void rejectedExecution(Runnable r,
							ThreadPoolExecutor executor) {
						Log.w(TAG, "SchedEx rejected submission of new task");
					}
				});
		player = createMediaPlayer();

		mediaButtonReceiver = new ComponentName(getPackageName(),
				MediaButtonReceiver.class.getName());
		audioManager.registerMediaButtonEventReceiver(mediaButtonReceiver);
		if (android.os.Build.VERSION.SDK_INT >= 14) {
			audioManager
					.registerRemoteControlClient(setupRemoteControlClient());
		}
		registerReceiver(headsetDisconnected, new IntentFilter(
				Intent.ACTION_HEADSET_PLUG));
		registerReceiver(shutdownReceiver, new IntentFilter(
				ACTION_SHUTDOWN_PLAYBACK_SERVICE));
		registerReceiver(audioBecomingNoisy, new IntentFilter(
				AudioManager.ACTION_AUDIO_BECOMING_NOISY));
		registerReceiver(skipCurrentEpisodeReceiver, new IntentFilter(
				ACTION_SKIP_CURRENT_EPISODE));

	}

	private MediaPlayer createMediaPlayer() {
		return createMediaPlayer(new MediaPlayer());
	}

	private MediaPlayer createMediaPlayer(MediaPlayer mp) {
		if (mp != null) {
			mp.setOnPreparedListener(preparedListener);
			mp.setOnCompletionListener(completionListener);
			mp.setOnSeekCompleteListener(onSeekCompleteListener);
			mp.setOnErrorListener(onErrorListener);
			mp.setOnBufferingUpdateListener(onBufferingUpdateListener);
			mp.setOnInfoListener(onInfoListener);
		}
		return mp;
	}

	@SuppressLint("NewApi")
	@Override
	public void onDestroy() {
		super.onDestroy();
		if (AppConfig.DEBUG)
			Log.d(TAG, "Service is about to be destroyed");
		isRunning = false;
		if (chapterLoader != null) {
			chapterLoader.interrupt();
		}
		disableSleepTimer();
		unregisterReceiver(headsetDisconnected);
		unregisterReceiver(shutdownReceiver);
		unregisterReceiver(audioBecomingNoisy);
		unregisterReceiver(skipCurrentEpisodeReceiver);
		if (android.os.Build.VERSION.SDK_INT >= 14) {
			audioManager.unregisterRemoteControlClient(remoteControlClient);
		}
		audioManager.unregisterMediaButtonEventReceiver(mediaButtonReceiver);
		audioManager.abandonAudioFocus(audioFocusChangeListener);
		player.release();
		stopWidgetUpdater();
		updateWidget();
	}

	@Override
	public IBinder onBind(Intent intent) {
		if (AppConfig.DEBUG)
			Log.d(TAG, "Received onBind event");
		return mBinder;
	}

	private final OnAudioFocusChangeListener audioFocusChangeListener = new OnAudioFocusChangeListener() {

		@Override
		public void onAudioFocusChange(int focusChange) {
			switch (focusChange) {
			case AudioManager.AUDIOFOCUS_LOSS:
				if (AppConfig.DEBUG)
					Log.d(TAG, "Lost audio focus");
				pause(true, false);
				stopSelf();
				break;
			case AudioManager.AUDIOFOCUS_GAIN:
				if (AppConfig.DEBUG)
					Log.d(TAG, "Gained audio focus");
				if (pausedBecauseOfTransientAudiofocusLoss) {
					audioManager.adjustStreamVolume(AudioManager.STREAM_MUSIC,
							AudioManager.ADJUST_RAISE, 0);
					play();
				}
				break;
			case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
				if (status == PlayerStatus.PLAYING) {
					if (AppConfig.DEBUG)
						Log.d(TAG, "Lost audio focus temporarily. Ducking...");
					audioManager.adjustStreamVolume(AudioManager.STREAM_MUSIC,
							AudioManager.ADJUST_LOWER, 0);
					pausedBecauseOfTransientAudiofocusLoss = true;
				}
				break;
			case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
				if (status == PlayerStatus.PLAYING) {
					if (AppConfig.DEBUG)
						Log.d(TAG, "Lost audio focus temporarily. Pausing...");
					pause(false, false);
					pausedBecauseOfTransientAudiofocusLoss = true;
				}
			}
		}
	};

	@Override
	public int onStartCommand(Intent intent, int flags, int startId) {
		super.onStartCommand(intent, flags, startId);

		if (AppConfig.DEBUG)
			Log.d(TAG, "OnStartCommand called");
		int keycode = intent.getIntExtra(MediaButtonReceiver.EXTRA_KEYCODE, -1);
		if (keycode != -1) {
			if (AppConfig.DEBUG)
				Log.d(TAG, "Received media button event");
			handleKeycode(keycode);
		} else {

			Playable playable = intent.getParcelableExtra(EXTRA_PLAYABLE);
			boolean playbackType = intent.getBooleanExtra(EXTRA_SHOULD_STREAM,
					true);
			if (playable == null) {
				Log.e(TAG, "Playable extra wasn't sent to the service");
				if (media == null) {
					stopSelf();
				}
				// Intent values appear to be valid
				// check if already playing and playbackType is the same
			} else if (media == null
					|| !playable.getIdentifier().equals(media.getIdentifier())
					|| playbackType != shouldStream) {
				pause(true, false);
				player.reset();
				sendNotificationBroadcast(NOTIFICATION_TYPE_RELOAD, 0);
				if (media == null
						|| !playable.getIdentifier().equals(
								media.getIdentifier())) {
					media = playable;
				}

				if (media != null) {
					shouldStream = playbackType;
					startWhenPrepared = intent.getBooleanExtra(
							EXTRA_START_WHEN_PREPARED, false);
					prepareImmediately = intent.getBooleanExtra(
							EXTRA_PREPARE_IMMEDIATELY, false);
					initMediaplayer();

				} else {
					Log.e(TAG, "Media is null");
					stopSelf();
				}

			} else if (media != null) {
				if (status == PlayerStatus.PAUSED) {
					play();
				}

			} else {
				Log.w(TAG, "Something went wrong. Shutting down...");
				stopSelf();
			}
		}
		return Service.START_NOT_STICKY;
	}

	/** Handles media button events */
	private void handleKeycode(int keycode) {
		if (AppConfig.DEBUG)
			Log.d(TAG, "Handling keycode: " + keycode);
		switch (keycode) {
		case KeyEvent.KEYCODE_HEADSETHOOK:
		case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE:
			if (status == PlayerStatus.PLAYING) {
				pause(true, true);
			} else if (status == PlayerStatus.PAUSED) {
				play();
			} else if (status == PlayerStatus.PREPARING) {
				setStartWhenPrepared(!startWhenPrepared);
			} else if (status == PlayerStatus.INITIALIZED) {
				startWhenPrepared = true;
				prepare();
			}
			break;
		case KeyEvent.KEYCODE_MEDIA_PLAY:
			if (status == PlayerStatus.PAUSED) {
				play();
			} else if (status == PlayerStatus.INITIALIZED) {
				startWhenPrepared = true;
				prepare();
			}
			break;
		case KeyEvent.KEYCODE_MEDIA_PAUSE:
			if (status == PlayerStatus.PLAYING) {
				pause(true, true);
			}
			break;
		}
	}

	/**
	 * Called by a mediaplayer Activity as soon as it has prepared its
	 * mediaplayer.
	 */
	public void setVideoSurface(SurfaceHolder sh) {
		if (AppConfig.DEBUG)
			Log.d(TAG, "Setting display");
		player.setDisplay(null);
		player.setDisplay(sh);
		if (status == PlayerStatus.STOPPED
				|| status == PlayerStatus.AWAITING_VIDEO_SURFACE) {
			try {
				InitTask initTask = new InitTask() {

					@Override
					protected void onPostExecute(Playable result) {<commit_msg>Enable Fast Forward and Rewind keys on Google TV remotes.<commit_after>package de.danoeh.antennapod.service;

import java.io.IOException;
import java.util.Date;
import java.util.concurrent.Future;
import java.util.concurrent.RejectedExecutionHandler;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

import android.annotation.SuppressLint;
import android.app.Notification;
import android.app.PendingIntent;
import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.media.AudioManager;
import android.media.AudioManager.OnAudioFocusChangeListener;
import android.media.MediaMetadataRetriever;
import android.media.MediaPlayer;
import android.media.RemoteControlClient;
import android.media.RemoteControlClient.MetadataEditor;
import android.os.AsyncTask;
import android.os.Binder;
import android.os.IBinder;
import android.preference.PreferenceManager;
import android.support.v4.app.NotificationCompat;
import android.util.Log;
import android.view.KeyEvent;
import android.view.SurfaceHolder;
import de.danoeh.antennapod.AppConfig;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.activity.AudioplayerActivity;
import de.danoeh.antennapod.activity.VideoplayerActivity;
import de.danoeh.antennapod.feed.Chapter;
import de.danoeh.antennapod.feed.FeedComponent;
import de.danoeh.antennapod.feed.FeedItem;
import de.danoeh.antennapod.feed.FeedManager;
import de.danoeh.antennapod.feed.FeedMedia;
import de.danoeh.antennapod.feed.MediaType;
import de.danoeh.antennapod.preferences.PlaybackPreferences;
import de.danoeh.antennapod.preferences.UserPreferences;
import de.danoeh.antennapod.receiver.MediaButtonReceiver;
import de.danoeh.antennapod.receiver.PlayerWidget;
import de.danoeh.antennapod.util.BitmapDecoder;
import de.danoeh.antennapod.util.flattr.FlattrUtils;
import de.danoeh.antennapod.util.playback.Playable;
import de.danoeh.antennapod.util.playback.Playable.PlayableException;

/** Controls the MediaPlayer that plays a FeedMedia-file */
public class PlaybackService extends Service {
	/** Logging tag */
	private static final String TAG = "PlaybackService";

	/** Parcelable of type Playable. */
	public static final String EXTRA_PLAYABLE = "PlaybackService.PlayableExtra";
	/** True if media should be streamed. */
	public static final String EXTRA_SHOULD_STREAM = "extra.de.danoeh.antennapod.service.shouldStream";
	/**
	 * True if playback should be started immediately after media has been
	 * prepared.
	 */
	public static final String EXTRA_START_WHEN_PREPARED = "extra.de.danoeh.antennapod.service.startWhenPrepared";

	public static final String EXTRA_PREPARE_IMMEDIATELY = "extra.de.danoeh.antennapod.service.prepareImmediately";

	public static final String ACTION_PLAYER_STATUS_CHANGED = "action.de.danoeh.antennapod.service.playerStatusChanged";
	private static final String AVRCP_ACTION_PLAYER_STATUS_CHANGED = "com.android.music.playstatechanged";

	public static final String ACTION_PLAYER_NOTIFICATION = "action.de.danoeh.antennapod.service.playerNotification";
	public static final String EXTRA_NOTIFICATION_CODE = "extra.de.danoeh.antennapod.service.notificationCode";
	public static final String EXTRA_NOTIFICATION_TYPE = "extra.de.danoeh.antennapod.service.notificationType";

	/**
	 * If the PlaybackService receives this action, it will stop playback and
	 * try to shutdown.
	 */
	public static final String ACTION_SHUTDOWN_PLAYBACK_SERVICE = "action.de.danoeh.antennapod.service.actionShutdownPlaybackService";

	/**
	 * If the PlaybackService receives this action, it will end playback of the
	 * current episode and load the next episode if there is one available.
	 * */
	public static final String ACTION_SKIP_CURRENT_EPISODE = "action.de.danoeh.antennapod.service.skipCurrentEpisode";

	/** Used in NOTIFICATION_TYPE_RELOAD. */
	public static final int EXTRA_CODE_AUDIO = 1;
	public static final int EXTRA_CODE_VIDEO = 2;

	public static final int NOTIFICATION_TYPE_ERROR = 0;
	public static final int NOTIFICATION_TYPE_INFO = 1;
	public static final int NOTIFICATION_TYPE_BUFFER_UPDATE = 2;
	public static final int NOTIFICATION_TYPE_RELOAD = 3;
	/** The state of the sleeptimer changed. */
	public static final int NOTIFICATION_TYPE_SLEEPTIMER_UPDATE = 4;
	public static final int NOTIFICATION_TYPE_BUFFER_START = 5;
	public static final int NOTIFICATION_TYPE_BUFFER_END = 6;
	/** No more episodes are going to be played. */
	public static final int NOTIFICATION_TYPE_PLAYBACK_END = 7;

	/**
	 * Returned by getPositionSafe() or getDurationSafe() if the playbackService
	 * is in an invalid state.
	 */
	public static final int INVALID_TIME = -1;

	/** Is true if service is running. */
	public static boolean isRunning = false;

	private static final int NOTIFICATION_ID = 1;

	private AudioManager audioManager;
	private ComponentName mediaButtonReceiver;

	private MediaPlayer player;
	private RemoteControlClient remoteControlClient;

	private Playable media;

	/** True if media should be streamed (Extracted from Intent Extra) . */
	private boolean shouldStream;

	/** True if service should prepare playback after it has been initialized */
	private boolean prepareImmediately;
	private boolean startWhenPrepared;
	private FeedManager manager;
	private PlayerStatus status;

	private PositionSaver positionSaver;
	private ScheduledFuture positionSaverFuture;

	private WidgetUpdateWorker widgetUpdater;
	private ScheduledFuture widgetUpdaterFuture;

	private SleepTimer sleepTimer;
	private Future sleepTimerFuture;

	private static final int SCHED_EX_POOL_SIZE = 3;
	private ScheduledThreadPoolExecutor schedExecutor;

	private volatile PlayerStatus statusBeforeSeek;

	private static boolean playingVideo;

	/** True if mediaplayer was paused because it lost audio focus temporarily */
	private boolean pausedBecauseOfTransientAudiofocusLoss;

	private Thread chapterLoader;

	private final IBinder mBinder = new LocalBinder();

	public class LocalBinder extends Binder {
		public PlaybackService getService() {
			return PlaybackService.this;
		}
	}

	@Override
	public boolean onUnbind(Intent intent) {
		if (AppConfig.DEBUG)
			Log.d(TAG, "Received onUnbind event");
		return super.onUnbind(intent);
	}

	/**
	 * Returns an intent which starts an audio- or videoplayer, depending on the
	 * type of media that is being played. If the playbackservice is not
	 * running, the type of the last played media will be looked up.
	 * */
	public static Intent getPlayerActivityIntent(Context context) {
		if (isRunning) {
			if (playingVideo) {
				return new Intent(context, VideoplayerActivity.class);
			} else {
				return new Intent(context, AudioplayerActivity.class);
			}
		} else {
			if (PlaybackPreferences.getCurrentEpisodeIsVideo()) {
				return new Intent(context, VideoplayerActivity.class);
			} else {
				return new Intent(context, AudioplayerActivity.class);
			}
		}
	}

	/**
	 * Same as getPlayerActivityIntent(context), but here the type of activity
	 * depends on the FeedMedia that is provided as an argument.
	 */
	public static Intent getPlayerActivityIntent(Context context, Playable media) {
		MediaType mt = media.getMediaType();
		if (mt == MediaType.VIDEO) {
			return new Intent(context, VideoplayerActivity.class);
		} else {
			return new Intent(context, AudioplayerActivity.class);
		}
	}

	@SuppressLint("NewApi")
	@Override
	public void onCreate() {
		super.onCreate();
		if (AppConfig.DEBUG)
			Log.d(TAG, "Service created.");
		isRunning = true;
		pausedBecauseOfTransientAudiofocusLoss = false;
		status = PlayerStatus.STOPPED;
		audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
		manager = FeedManager.getInstance();
		schedExecutor = new ScheduledThreadPoolExecutor(SCHED_EX_POOL_SIZE,
				new ThreadFactory() {

					@Override
					public Thread newThread(Runnable r) {
						Thread t = new Thread(r);
						t.setPriority(Thread.MIN_PRIORITY);
						return t;
					}
				}, new RejectedExecutionHandler() {

					@Override
					public void rejectedExecution(Runnable r,
							ThreadPoolExecutor executor) {
						Log.w(TAG, "SchedEx rejected submission of new task");
					}
				});
		player = createMediaPlayer();

		mediaButtonReceiver = new ComponentName(getPackageName(),
				MediaButtonReceiver.class.getName());
		audioManager.registerMediaButtonEventReceiver(mediaButtonReceiver);
		if (android.os.Build.VERSION.SDK_INT >= 14) {
			audioManager
					.registerRemoteControlClient(setupRemoteControlClient());
		}
		registerReceiver(headsetDisconnected, new IntentFilter(
				Intent.ACTION_HEADSET_PLUG));
		registerReceiver(shutdownReceiver, new IntentFilter(
				ACTION_SHUTDOWN_PLAYBACK_SERVICE));
		registerReceiver(audioBecomingNoisy, new IntentFilter(
				AudioManager.ACTION_AUDIO_BECOMING_NOISY));
		registerReceiver(skipCurrentEpisodeReceiver, new IntentFilter(
				ACTION_SKIP_CURRENT_EPISODE));

	}

	private MediaPlayer createMediaPlayer() {
		return createMediaPlayer(new MediaPlayer());
	}

	private MediaPlayer createMediaPlayer(MediaPlayer mp) {
		if (mp != null) {
			mp.setOnPreparedListener(preparedListener);
			mp.setOnCompletionListener(completionListener);
			mp.setOnSeekCompleteListener(onSeekCompleteListener);
			mp.setOnErrorListener(onErrorListener);
			mp.setOnBufferingUpdateListener(onBufferingUpdateListener);
			mp.setOnInfoListener(onInfoListener);
		}
		return mp;
	}

	@SuppressLint("NewApi")
	@Override
	public void onDestroy() {
		super.onDestroy();
		if (AppConfig.DEBUG)
			Log.d(TAG, "Service is about to be destroyed");
		isRunning = false;
		if (chapterLoader != null) {
			chapterLoader.interrupt();
		}
		disableSleepTimer();
		unregisterReceiver(headsetDisconnected);
		unregisterReceiver(shutdownReceiver);
		unregisterReceiver(audioBecomingNoisy);
		unregisterReceiver(skipCurrentEpisodeReceiver);
		if (android.os.Build.VERSION.SDK_INT >= 14) {
			audioManager.unregisterRemoteControlClient(remoteControlClient);
		}
		audioManager.unregisterMediaButtonEventReceiver(mediaButtonReceiver);
		audioManager.abandonAudioFocus(audioFocusChangeListener);
		player.release();
		stopWidgetUpdater();
		updateWidget();
	}

	@Override
	public IBinder onBind(Intent intent) {
		if (AppConfig.DEBUG)
			Log.d(TAG, "Received onBind event");
		return mBinder;
	}

	private final OnAudioFocusChangeListener audioFocusChangeListener = new OnAudioFocusChangeListener() {

		@Override
		public void onAudioFocusChange(int focusChange) {
			switch (focusChange) {
			case AudioManager.AUDIOFOCUS_LOSS:
				if (AppConfig.DEBUG)
					Log.d(TAG, "Lost audio focus");
				pause(true, false);
				stopSelf();
				break;
			case AudioManager.AUDIOFOCUS_GAIN:
				if (AppConfig.DEBUG)
					Log.d(TAG, "Gained audio focus");
				if (pausedBecauseOfTransientAudiofocusLoss) {
					audioManager.adjustStreamVolume(AudioManager.STREAM_MUSIC,
							AudioManager.ADJUST_RAISE, 0);
					play();
				}
				break;
			case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
				if (status == PlayerStatus.PLAYING) {
					if (AppConfig.DEBUG)
						Log.d(TAG, "Lost audio focus temporarily. Ducking...");
					audioManager.adjustStreamVolume(AudioManager.STREAM_MUSIC,
							AudioManager.ADJUST_LOWER, 0);
					pausedBecauseOfTransientAudiofocusLoss = true;
				}
				break;
			case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
				if (status == PlayerStatus.PLAYING) {
					if (AppConfig.DEBUG)
						Log.d(TAG, "Lost audio focus temporarily. Pausing...");
					pause(false, false);
					pausedBecauseOfTransientAudiofocusLoss = true;
				}
			}
		}
	};

	@Override
	public int onStartCommand(Intent intent, int flags, int startId) {
		super.onStartCommand(intent, flags, startId);

		if (AppConfig.DEBUG)
			Log.d(TAG, "OnStartCommand called");
		int keycode = intent.getIntExtra(MediaButtonReceiver.EXTRA_KEYCODE, -1);
		if (keycode != -1) {
			if (AppConfig.DEBUG)
				Log.d(TAG, "Received media button event");
			handleKeycode(keycode);
		} else {

			Playable playable = intent.getParcelableExtra(EXTRA_PLAYABLE);
			boolean playbackType = intent.getBooleanExtra(EXTRA_SHOULD_STREAM,
					true);
			if (playable == null) {
				Log.e(TAG, "Playable extra wasn't sent to the service");
				if (media == null) {
					stopSelf();
				}
				// Intent values appear to be valid
				// check if already playing and playbackType is the same
			} else if (media == null
					|| !playable.getIdentifier().equals(media.getIdentifier())
					|| playbackType != shouldStream) {
				pause(true, false);
				player.reset();
				sendNotificationBroadcast(NOTIFICATION_TYPE_RELOAD, 0);
				if (media == null
						|| !playable.getIdentifier().equals(
								media.getIdentifier())) {
					media = playable;
				}

				if (media != null) {
					shouldStream = playbackType;
					startWhenPrepared = intent.getBooleanExtra(
							EXTRA_START_WHEN_PREPARED, false);
					prepareImmediately = intent.getBooleanExtra(
							EXTRA_PREPARE_IMMEDIATELY, false);
					initMediaplayer();

				} else {
					Log.e(TAG, "Media is null");
					stopSelf();
				}

			} else if (media != null) {
				if (status == PlayerStatus.PAUSED) {
					play();
				}

			} else {
				Log.w(TAG, "Something went wrong. Shutting down...");
				stopSelf();
			}
		}
		return Service.START_NOT_STICKY;
	}

	/** Handles media button events */
	private void handleKeycode(int keycode) {
		if (AppConfig.DEBUG)
			Log.d(TAG, "Handling keycode: " + keycode);
		switch (keycode) {
		case KeyEvent.KEYCODE_HEADSETHOOK:
		case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE:
			if (status == PlayerStatus.PLAYING) {
				pause(true, true);
			} else if (status == PlayerStatus.PAUSED) {
				play();
			} else if (status == PlayerStatus.PREPARING) {
				setStartWhenPrepared(!startWhenPrepared);
			} else if (status == PlayerStatus.INITIALIZED) {
				startWhenPrepared = true;
				prepare();
			}
			break;
		case KeyEvent.KEYCODE_MEDIA_PLAY:
			if (status == PlayerStatus.PAUSED) {
				play();
			} else if (status == PlayerStatus.INITIALIZED) {
				startWhenPrepared = true;
				prepare();
			}
			break;
		case KeyEvent.KEYCODE_MEDIA_PAUSE:
			if (status == PlayerStatus.PLAYING) {
				pause(true, true);
			}
			break;
		case KeyEvent.KEYCODE_MEDIA_FAST_FORWARD: {
			int currentPos = getCurrentPositionSafe();
			int duration = getDurationSafe();

			if (currentPos != INVALID_TIME && duration != INVALID_TIME) {
				if (currentPos < duration) {
					seek(currentPos + 10000);
				}
			}
			break;
		}
		case KeyEvent.KEYCODE_MEDIA_REWIND: {
			int currentPos = getCurrentPositionSafe();
			int duration = getDurationSafe();

			if (currentPos != INVALID_TIME && duration != INVALID_TIME) {
				if (currentPos > 10000) {
					seek(currentPos - 10000);
				}
			}
			break;
 		  }
		} 
	}

	/**
	 * Called by a mediaplayer Activity as soon as it has prepared its
	 * mediaplayer.
	 */
	public void setVideoSurface(SurfaceHolder sh) {
		if (AppConfig.DEBUG)
			Log.d(TAG, "Setting display");
		player.setDisplay(null);
		player.setDisplay(sh);
		if (status == PlayerStatus.STOPPED
				|| status == PlayerStatus.AWAITING_VIDEO_SURFACE) {
			try {
				InitTask initTask = new InitTask() {

					@Override
					protected void onPostExecute(Playable result) {
{'fraction_non_alphanumeric': 0.0929662208560871, 'fraction_numerical': 0.001994702593113371, 'mean_word_length': 4.574553408676631, 'pattern_counts': {'":': 0, '<': 4, '<?xml version=': 0, '>': 8, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 8, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '4887917', 'n_tokens_mistral': 10609, 'n_tokens_neox': 9179, 'n_words': 2177}