summaryrefslogtreecommitdiff
path: root/src/de/danoeh/antennapod/util/playback
diff options
context:
space:
mode:
Diffstat (limited to 'src/de/danoeh/antennapod/util/playback')
-rw-r--r--src/de/danoeh/antennapod/util/playback/ExternalMedia.java238
-rw-r--r--src/de/danoeh/antennapod/util/playback/MediaPlayerError.java23
-rw-r--r--src/de/danoeh/antennapod/util/playback/Playable.java197
-rw-r--r--src/de/danoeh/antennapod/util/playback/PlaybackController.java679
4 files changed, 1137 insertions, 0 deletions
diff --git a/src/de/danoeh/antennapod/util/playback/ExternalMedia.java b/src/de/danoeh/antennapod/util/playback/ExternalMedia.java
new file mode 100644
index 000000000..4574f088e
--- /dev/null
+++ b/src/de/danoeh/antennapod/util/playback/ExternalMedia.java
@@ -0,0 +1,238 @@
+package de.danoeh.antennapod.util.playback;
+
+import java.io.BufferedOutputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.List;
+
+import org.apache.commons.io.IOUtils;
+
+import android.content.SharedPreferences;
+import android.content.SharedPreferences.Editor;
+import android.media.MediaMetadataRetriever;
+import android.os.Parcel;
+import android.os.Parcelable;
+import de.danoeh.antennapod.PodcastApp;
+import de.danoeh.antennapod.feed.Chapter;
+import de.danoeh.antennapod.feed.MediaType;
+import de.danoeh.antennapod.util.ChapterUtils;
+import de.danoeh.antennapod.util.FileNameGenerator;
+
+/** Represents a media file that is stored on the local storage device. */
+public class ExternalMedia implements Playable {
+
+ public static final int PLAYABLE_TYPE_EXTERNAL_MEDIA = 2;
+ public static final String PREF_SOURCE_URL = "ExternalMedia.PrefSourceUrl";
+ public static final String PREF_POSITION = "ExternalMedia.PrefPosition";
+ public static final String PREF_MEDIA_TYPE = "ExternalMedia.PrefMediaType";
+
+ private String source;
+
+ private String episodeTitle;
+ private String feedTitle;
+ private String shownotes;
+ private MediaType mediaType = MediaType.AUDIO;
+ private List<Chapter> chapters;
+ private String imageUrl;
+ private int duration;
+ private int position;
+
+ public ExternalMedia(String source, MediaType mediaType) {
+ super();
+ this.source = source;
+ this.mediaType = mediaType;
+ }
+
+ public ExternalMedia(String source, MediaType mediaType, int position) {
+ this(source, mediaType);
+ this.position = position;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeString(source);
+ dest.writeString(mediaType.toString());
+ dest.writeInt(position);
+ }
+
+ @Override
+ public void writeToPreferences(Editor prefEditor) {
+ prefEditor.putString(PREF_SOURCE_URL, source);
+ prefEditor.putString(PREF_MEDIA_TYPE, mediaType.toString());
+ prefEditor.putInt(PREF_POSITION, position);
+ }
+
+ @Override
+ public void loadMetadata() throws PlayableException {
+ MediaMetadataRetriever mmr = new MediaMetadataRetriever();
+ try {
+ mmr.setDataSource(source);
+ } catch (IllegalArgumentException e) {
+ e.printStackTrace();
+ throw new PlayableException("IllegalArgumentException when setting up MediaMetadataReceiver");
+ }
+ episodeTitle = mmr
+ .extractMetadata(MediaMetadataRetriever.METADATA_KEY_TITLE);
+ feedTitle = mmr
+ .extractMetadata(MediaMetadataRetriever.METADATA_KEY_ALBUM);
+ duration = Integer.parseInt(mmr
+ .extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION));
+ ChapterUtils.loadChaptersFromFileUrl(this);
+ byte[] imgData = mmr.getEmbeddedPicture();
+ File cacheDir = PodcastApp.getInstance().getExternalCacheDir();
+ if (cacheDir != null) {
+ OutputStream out = null;
+ try {
+ File tmpFile = File.createTempFile(
+ FileNameGenerator.generateFileName(source) + "-img",
+ null, cacheDir);
+ out = new BufferedOutputStream(new FileOutputStream(tmpFile));
+ IOUtils.write(imgData, out);
+ imageUrl = tmpFile.getAbsolutePath();
+ } catch (IOException e) {
+ e.printStackTrace();
+ throw new PlayableException("IOException during loadMetadata()");
+ } finally {
+ IOUtils.closeQuietly(out);
+ }
+ }
+ }
+
+ @Override
+ public String getEpisodeTitle() {
+ return episodeTitle;
+ }
+
+ @Override
+ public void loadShownotes(ShownoteLoaderCallback callback) {
+ callback.onShownotesLoaded(null);
+ }
+
+ @Override
+ public List<Chapter> getChapters() {
+ return chapters;
+ }
+
+ @Override
+ public String getWebsiteLink() {
+ return null;
+ }
+
+ @Override
+ public String getPaymentLink() {
+ return null;
+ }
+
+ @Override
+ public String getFeedTitle() {
+ return feedTitle;
+ }
+
+ @Override
+ public String getImageFileUrl() {
+ return imageUrl;
+ }
+
+ @Override
+ public Object getIdentifier() {
+ return source;
+ }
+
+ @Override
+ public int getDuration() {
+ return duration;
+ }
+
+ @Override
+ public int getPosition() {
+ return position;
+ }
+
+ @Override
+ public MediaType getMediaType() {
+ return mediaType;
+ }
+
+ @Override
+ public String getLocalMediaUrl() {
+ return source;
+ }
+
+ @Override
+ public String getStreamUrl() {
+ return null;
+ }
+
+ @Override
+ public boolean localFileAvailable() {
+ return true;
+ }
+
+ @Override
+ public boolean streamAvailable() {
+ return false;
+ }
+
+ @Override
+ public void saveCurrentPosition(SharedPreferences pref, int newPosition) {
+ SharedPreferences.Editor editor = pref.edit();
+ editor.putInt(PREF_POSITION, newPosition);
+ position = newPosition;
+ editor.commit();
+ }
+
+ @Override
+ public void setPosition(int newPosition) {
+ position = newPosition;
+ }
+
+ @Override
+ public void setDuration(int newDuration) {
+ duration = newDuration;
+ }
+
+ @Override
+ public void onPlaybackStart() {
+
+ }
+
+ @Override
+ public void onPlaybackCompleted() {
+
+ }
+
+ @Override
+ public int getPlayableType() {
+ return PLAYABLE_TYPE_EXTERNAL_MEDIA;
+ }
+
+ @Override
+ public void setChapters(List<Chapter> chapters) {
+ this.chapters = chapters;
+ }
+
+ public static final Parcelable.Creator<ExternalMedia> CREATOR = new Parcelable.Creator<ExternalMedia>() {
+ public ExternalMedia createFromParcel(Parcel in) {
+ String source = in.readString();
+ MediaType type = MediaType.valueOf(in.readString());
+ int position = 0;
+ if (in.dataAvail() > 0) {
+ position = in.readInt();
+ }
+ ExternalMedia extMedia = new ExternalMedia(source, type, position);
+ return extMedia;
+ }
+
+ public ExternalMedia[] newArray(int size) {
+ return new ExternalMedia[size];
+ }
+ };
+
+}
diff --git a/src/de/danoeh/antennapod/util/playback/MediaPlayerError.java b/src/de/danoeh/antennapod/util/playback/MediaPlayerError.java
new file mode 100644
index 000000000..23ead581f
--- /dev/null
+++ b/src/de/danoeh/antennapod/util/playback/MediaPlayerError.java
@@ -0,0 +1,23 @@
+package de.danoeh.antennapod.util.playback;
+
+import android.content.Context;
+import android.media.MediaPlayer;
+import de.danoeh.antennapod.R;
+
+/** Utility class for MediaPlayer errors. */
+public class MediaPlayerError {
+
+ /** Get a human-readable string for a specific error code. */
+ public static String getErrorString(Context context, int code) {
+ int resId;
+ switch(code) {
+ case MediaPlayer.MEDIA_ERROR_SERVER_DIED:
+ resId = R.string.playback_error_server_died;
+ break;
+ default:
+ resId = R.string.playback_error_unknown;
+ break;
+ }
+ return context.getString(resId);
+ }
+}
diff --git a/src/de/danoeh/antennapod/util/playback/Playable.java b/src/de/danoeh/antennapod/util/playback/Playable.java
new file mode 100644
index 000000000..bf38b9518
--- /dev/null
+++ b/src/de/danoeh/antennapod/util/playback/Playable.java
@@ -0,0 +1,197 @@
+package de.danoeh.antennapod.util.playback;
+
+import java.util.List;
+
+import android.content.SharedPreferences;
+import android.os.Parcelable;
+import android.util.Log;
+import de.danoeh.antennapod.feed.Chapter;
+import de.danoeh.antennapod.feed.Feed;
+import de.danoeh.antennapod.feed.FeedManager;
+import de.danoeh.antennapod.feed.FeedMedia;
+import de.danoeh.antennapod.feed.MediaType;
+
+/** Interface for objects that can be played by the PlaybackService. */
+public interface Playable extends Parcelable {
+
+ /**
+ * Save information about the playable in a preference so that it can be
+ * restored later via PlayableUtils.createInstanceFromPreferences.
+ * Implementations must NOT call commit() after they have written the values
+ * to the preferences file.
+ */
+ public void writeToPreferences(SharedPreferences.Editor prefEditor);
+
+ /**
+ * This method is called from a separate thread by the PlaybackService.
+ * Playable objects should load their metadata in this method (for example:
+ * chapter marks).
+ */
+ public void loadMetadata() throws PlayableException;
+
+ /** Returns the title of the episode that this playable represents */
+ public String getEpisodeTitle();
+
+ /**
+ * Loads shownotes. If the shownotes have to be loaded from a file or from a
+ * database, it should be done in a separate thread. After the shownotes
+ * have been loaded, callback.onShownotesLoaded should be called.
+ */
+ public void loadShownotes(ShownoteLoaderCallback callback);
+
+ /**
+ * Returns a list of chapter marks or null if this Playable has no chapters.
+ */
+ public List<Chapter> getChapters();
+
+ /** Returns a link to a website that is meant to be shown in a browser */
+ public String getWebsiteLink();
+
+ public String getPaymentLink();
+
+ /** Returns the title of the feed this Playable belongs to. */
+ public String getFeedTitle();
+
+ /** Returns a file url to an image or null if no such image exists. */
+ public String getImageFileUrl();
+
+ /**
+ * Returns a unique identifier, for example a file url or an ID from a
+ * database.
+ */
+ public Object getIdentifier();
+
+ /** Return duration of object or 0 if duration is unknown. */
+ public int getDuration();
+
+ /** Return position of object or 0 if position is unknown. */
+ public int getPosition();
+
+ /**
+ * Returns the type of media. This method should return the correct value
+ * BEFORE loadMetadata() is called.
+ */
+ public MediaType getMediaType();
+
+ /**
+ * Returns an url to a local file that can be played or null if this file
+ * does not exist.
+ */
+ public String getLocalMediaUrl();
+
+ /**
+ * Returns an url to a file that can be streamed by the player or null if
+ * this url is not known.
+ */
+ public String getStreamUrl();
+
+ /**
+ * Returns true if a local file that can be played is available. getFileUrl
+ * MUST return a non-null string if this method returns true.
+ */
+ public boolean localFileAvailable();
+
+ /**
+ * Returns true if a streamable file is available. getStreamUrl MUST return
+ * a non-null string if this method returns true.
+ */
+ public boolean streamAvailable();
+
+ /**
+ * Saves the current position of this object. Implementations can use the
+ * provided SharedPreference to save this information and retrieve it later
+ * via PlayableUtils.createInstanceFromPreferences.
+ */
+ public void saveCurrentPosition(SharedPreferences pref, int newPosition);
+
+ public void setPosition(int newPosition);
+
+ public void setDuration(int newDuration);
+
+ /** Is called by the PlaybackService when playback starts. */
+ public void onPlaybackStart();
+
+ /** Is called by the PlaybackService when playback is completed. */
+ public void onPlaybackCompleted();
+
+ /**
+ * Returns an integer that must be unique among all Playable classes. The
+ * return value is later used by PlayableUtils to determine the type of the
+ * Playable object that is restored.
+ */
+ public int getPlayableType();
+
+ public void setChapters(List<Chapter> chapters);
+
+ /** Provides utility methods for Playable objects. */
+ public static class PlayableUtils {
+ private static final String TAG = "PlayableUtils";
+
+ /**
+ * Restores a playable object from a sharedPreferences file.
+ *
+ * @param type
+ * An integer that represents the type of the Playable object
+ * that is restored.
+ * @param pref
+ * The SharedPreferences file from which the Playable object
+ * is restored
+ * @return The restored Playable object
+ */
+ public static Playable createInstanceFromPreferences(int type,
+ SharedPreferences pref) {
+ // ADD new Playable types here:
+ switch (type) {
+ case FeedMedia.PLAYABLE_TYPE_FEEDMEDIA:
+ long feedId = pref.getLong(FeedMedia.PREF_FEED_ID, -1);
+ long mediaId = pref.getLong(FeedMedia.PREF_MEDIA_ID, -1);
+ if (feedId != -1 && mediaId != -1) {
+ Feed feed = FeedManager.getInstance().getFeed(feedId);
+ if (feed != null) {
+ return FeedManager.getInstance().getFeedMedia(mediaId,
+ feed);
+ }
+ }
+ break;
+ case ExternalMedia.PLAYABLE_TYPE_EXTERNAL_MEDIA:
+ String source = pref.getString(ExternalMedia.PREF_SOURCE_URL,
+ null);
+ String mediaType = pref.getString(
+ ExternalMedia.PREF_MEDIA_TYPE, null);
+ if (source != null && mediaType != null) {
+ int position = pref.getInt(ExternalMedia.PREF_POSITION, 0);
+ return new ExternalMedia(source,
+ MediaType.valueOf(mediaType), position);
+ }
+ break;
+ }
+ Log.e(TAG, "Could not restore Playable object from preferences");
+ return null;
+ }
+ }
+
+ public static class PlayableException extends Exception {
+ private static final long serialVersionUID = 1L;
+
+ public PlayableException() {
+ super();
+ }
+
+ public PlayableException(String detailMessage, Throwable throwable) {
+ super(detailMessage, throwable);
+ }
+
+ public PlayableException(String detailMessage) {
+ super(detailMessage);
+ }
+
+ public PlayableException(Throwable throwable) {
+ super(throwable);
+ }
+
+ }
+
+ public static interface ShownoteLoaderCallback {
+ void onShownotesLoaded(String shownotes);
+ }
+}
diff --git a/src/de/danoeh/antennapod/util/playback/PlaybackController.java b/src/de/danoeh/antennapod/util/playback/PlaybackController.java
new file mode 100644
index 000000000..699ff6699
--- /dev/null
+++ b/src/de/danoeh/antennapod/util/playback/PlaybackController.java
@@ -0,0 +1,679 @@
+package de.danoeh.antennapod.util.playback;
+
+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.app.Activity;
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.ServiceConnection;
+import android.content.SharedPreferences;
+import android.content.res.TypedArray;
+import android.os.IBinder;
+import android.preference.PreferenceManager;
+import android.util.Log;
+import android.view.SurfaceHolder;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.ImageButton;
+import android.widget.SeekBar;
+import android.widget.TextView;
+import de.danoeh.antennapod.AppConfig;
+import de.danoeh.antennapod.R;
+import de.danoeh.antennapod.feed.Chapter;
+import de.danoeh.antennapod.feed.FeedManager;
+import de.danoeh.antennapod.feed.FeedMedia;
+import de.danoeh.antennapod.preferences.PlaybackPreferences;
+import de.danoeh.antennapod.service.PlaybackService;
+import de.danoeh.antennapod.service.PlayerStatus;
+import de.danoeh.antennapod.util.Converter;
+import de.danoeh.antennapod.util.playback.Playable.PlayableUtils;
+
+/**
+ * Communicates with the playback service. GUI classes should use this class to
+ * control playback instead of communicating with the PlaybackService directly.
+ */
+public abstract class PlaybackController {
+ private static final String TAG = "PlaybackController";
+
+ static final int DEFAULT_SEEK_DELTA = 30000;
+ public static final int INVALID_TIME = -1;
+
+ private Activity activity;
+
+ private PlaybackService playbackService;
+ private Playable media;
+ private PlayerStatus status;
+
+ private ScheduledThreadPoolExecutor schedExecutor;
+ private static final int SCHED_EX_POOLSIZE = 1;
+
+ protected MediaPositionObserver positionObserver;
+ protected ScheduledFuture positionObserverFuture;
+
+ private boolean mediaInfoLoaded = false;
+ private boolean released = false;
+
+ /**
+ * True if controller should reinit playback service if 'pause' button is
+ * pressed.
+ */
+ private boolean reinitOnPause;
+
+ public PlaybackController(Activity activity, boolean reinitOnPause) {
+ this.activity = activity;
+ this.reinitOnPause = reinitOnPause;
+ schedExecutor = new ScheduledThreadPoolExecutor(SCHED_EX_POOLSIZE,
+ 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,
+ "Rejected execution of runnable in schedExecutor");
+ }
+ });
+ }
+
+ /**
+ * Creates a new connection to the playbackService. Should be called in the
+ * activity's onResume() method.
+ */
+ public void init() {
+ if (!released) {
+ bindToService();
+ } else {
+ throw new IllegalStateException(
+ "Can't call init() after release() has been called");
+ }
+ }
+
+ /**
+ * Should be called if the PlaybackController is no longer needed, for
+ * example in the activity's onStop() method.
+ */
+ public void release() {
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Releasing PlaybackController");
+
+ try {
+ activity.unregisterReceiver(statusUpdate);
+ } catch (IllegalArgumentException e) {
+ // ignore
+ }
+
+ try {
+ activity.unregisterReceiver(notificationReceiver);
+ } catch (IllegalArgumentException e) {
+ // ignore
+ }
+
+ try {
+ activity.unbindService(mConnection);
+ } catch (IllegalArgumentException e) {
+ // ignore
+ }
+
+ try {
+ activity.unregisterReceiver(shutdownReceiver);
+ } catch (IllegalArgumentException e) {
+ // ignore
+ }
+ cancelPositionObserver();
+ schedExecutor.shutdownNow();
+ media = null;
+ released = true;
+
+ }
+
+ /** Should be called in the activity's onPause() method. */
+ public void pause() {
+ mediaInfoLoaded = false;
+ if (playbackService != null && playbackService.isPlayingVideo()) {
+ playbackService.pause(true, true);
+ }
+ }
+
+ /**
+ * Tries to establish a connection to the PlaybackService. If it isn't
+ * running, the PlaybackService will be started with the last played media
+ * as the arguments of the launch intent.
+ */
+ private void bindToService() {
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Trying to connect to service");
+ Intent serviceIntent = getPlayLastPlayedMediaIntent();
+ boolean bound = false;
+ if (!PlaybackService.isRunning) {
+ if (serviceIntent != null) {
+ activity.startService(serviceIntent);
+ bound = activity.bindService(serviceIntent, mConnection, 0);
+ } else {
+ status = PlayerStatus.STOPPED;
+ setupGUI();
+ handleStatus();
+ }
+ } else {
+ if (AppConfig.DEBUG)
+ Log.d(TAG,
+ "PlaybackService is running, trying to connect without start command.");
+ bound = activity.bindService(new Intent(activity,
+ PlaybackService.class), mConnection, 0);
+ }
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Result for service binding: " + bound);
+ }
+
+ /**
+ * Returns an intent that starts the PlaybackService and plays the last
+ * played media or null if no last played media could be found.
+ */
+ private Intent getPlayLastPlayedMediaIntent() {
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Trying to restore last played media");
+ SharedPreferences prefs = PreferenceManager
+ .getDefaultSharedPreferences(activity.getApplicationContext());
+ long lastPlayedId = PlaybackPreferences.getLastPlayedId();
+ if (lastPlayedId != PlaybackPreferences.NO_MEDIA_PLAYING) {
+ Playable media = PlayableUtils.createInstanceFromPreferences((int) lastPlayedId, prefs);
+ if (media != null) {
+ Intent serviceIntent = new Intent(activity,
+ PlaybackService.class);
+ serviceIntent.putExtra(PlaybackService.EXTRA_PLAYABLE, media);
+ serviceIntent.putExtra(
+ PlaybackService.EXTRA_START_WHEN_PREPARED, false);
+ serviceIntent.putExtra(
+ PlaybackService.EXTRA_PREPARE_IMMEDIATELY, false);
+ boolean fileExists = media.localFileAvailable();
+ boolean lastIsStream = PlaybackPreferences.isLastIsStream();
+ if (!fileExists && !lastIsStream && media instanceof FeedMedia) {
+ FeedManager.getInstance().notifyMissingFeedMediaFile(
+ activity, (FeedMedia) media);
+ }
+ serviceIntent.putExtra(PlaybackService.EXTRA_SHOULD_STREAM,
+ lastIsStream || !fileExists);
+ return serviceIntent;
+ }
+ }
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "No last played media found");
+ return null;
+ }
+
+ public abstract void setupGUI();
+
+ private void setupPositionObserver() {
+ if ((positionObserverFuture != null && positionObserverFuture
+ .isCancelled())
+ || (positionObserverFuture != null && positionObserverFuture
+ .isDone()) || positionObserverFuture == null) {
+
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Setting up position observer");
+ positionObserver = new MediaPositionObserver();
+ positionObserverFuture = schedExecutor.scheduleWithFixedDelay(
+ positionObserver, MediaPositionObserver.WAITING_INTERVALL,
+ MediaPositionObserver.WAITING_INTERVALL,
+ TimeUnit.MILLISECONDS);
+ }
+ }
+
+ private void cancelPositionObserver() {
+ if (positionObserverFuture != null) {
+ boolean result = positionObserverFuture.cancel(true);
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "PositionObserver cancelled. Result: " + result);
+ }
+ }
+
+ public abstract void onPositionObserverUpdate();
+
+ private ServiceConnection mConnection = new ServiceConnection() {
+ public void onServiceConnected(ComponentName className, IBinder service) {
+ playbackService = ((PlaybackService.LocalBinder) service)
+ .getService();
+
+ activity.registerReceiver(statusUpdate, new IntentFilter(
+ PlaybackService.ACTION_PLAYER_STATUS_CHANGED));
+
+ activity.registerReceiver(notificationReceiver, new IntentFilter(
+ PlaybackService.ACTION_PLAYER_NOTIFICATION));
+
+ activity.registerReceiver(shutdownReceiver, new IntentFilter(
+ PlaybackService.ACTION_SHUTDOWN_PLAYBACK_SERVICE));
+
+ queryService();
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Connection to Service established");
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName name) {
+ playbackService = null;
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Disconnected from Service");
+
+ }
+ };
+
+ protected BroadcastReceiver statusUpdate = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Received statusUpdate Intent.");
+ if (playbackService != null) {
+ status = playbackService.getStatus();
+ handleStatus();
+ } else {
+ Log.w(TAG,
+ "Couldn't receive status update: playbackService was null");
+ }
+ }
+ };
+
+ protected BroadcastReceiver notificationReceiver = new BroadcastReceiver() {
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ int type = intent.getIntExtra(
+ PlaybackService.EXTRA_NOTIFICATION_TYPE, -1);
+ int code = intent.getIntExtra(
+ PlaybackService.EXTRA_NOTIFICATION_CODE, -1);
+ if (code != -1 && type != -1) {
+ switch (type) {
+ case PlaybackService.NOTIFICATION_TYPE_ERROR:
+ handleError(code);
+ break;
+ case PlaybackService.NOTIFICATION_TYPE_BUFFER_UPDATE:
+ float progress = ((float) code) / 100;
+ onBufferUpdate(progress);
+ break;
+ case PlaybackService.NOTIFICATION_TYPE_RELOAD:
+ cancelPositionObserver();
+ mediaInfoLoaded = false;
+ onReloadNotification(intent.getIntExtra(
+ PlaybackService.EXTRA_NOTIFICATION_CODE, -1));
+ queryService();
+
+ break;
+ case PlaybackService.NOTIFICATION_TYPE_SLEEPTIMER_UPDATE:
+ onSleepTimerUpdate();
+ break;
+ case PlaybackService.NOTIFICATION_TYPE_BUFFER_START:
+ onBufferStart();
+ break;
+ case PlaybackService.NOTIFICATION_TYPE_BUFFER_END:
+ onBufferEnd();
+ break;
+ }
+
+ } else {
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Bad arguments. Won't handle intent");
+ }
+
+ }
+
+ };
+
+ private BroadcastReceiver shutdownReceiver = new BroadcastReceiver() {
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (intent.getAction().equals(
+ PlaybackService.ACTION_SHUTDOWN_PLAYBACK_SERVICE)) {
+ release();
+ onShutdownNotification();
+ }
+ }
+ };
+
+ public abstract void onShutdownNotification();
+
+ /** Called when the currently displayed information should be refreshed. */
+ public abstract void onReloadNotification(int code);
+
+ public abstract void onBufferStart();
+
+ public abstract void onBufferEnd();
+
+ public abstract void onBufferUpdate(float progress);
+
+ public abstract void onSleepTimerUpdate();
+
+ public abstract void handleError(int code);
+
+ /**
+ * Is called whenever the PlaybackService changes it's status. This method
+ * should be used to update the GUI or start/cancel background threads.
+ */
+ private void handleStatus() {
+ TypedArray res = activity.obtainStyledAttributes(new int[] {
+ R.attr.av_play, R.attr.av_pause });
+ final int playResource = res.getResourceId(0, R.drawable.av_play);
+ final int pauseResource = res.getResourceId(1, R.drawable.av_pause);
+ res.recycle();
+
+ switch (status) {
+
+ case ERROR:
+ postStatusMsg(R.string.player_error_msg);
+ break;
+ case PAUSED:
+ clearStatusMsg();
+ checkMediaInfoLoaded();
+ cancelPositionObserver();
+ updatePlayButtonAppearance(playResource);
+ break;
+ case PLAYING:
+ clearStatusMsg();
+ checkMediaInfoLoaded();
+ setupPositionObserver();
+ updatePlayButtonAppearance(pauseResource);
+ break;
+ case PREPARING:
+ postStatusMsg(R.string.player_preparing_msg);
+ checkMediaInfoLoaded();
+ if (playbackService != null) {
+ if (playbackService.isStartWhenPrepared()) {
+ updatePlayButtonAppearance(pauseResource);
+ } else {
+ updatePlayButtonAppearance(playResource);
+ }
+ }
+ break;
+ case STOPPED:
+ postStatusMsg(R.string.player_stopped_msg);
+ break;
+ case PREPARED:
+ checkMediaInfoLoaded();
+ postStatusMsg(R.string.player_ready_msg);
+ updatePlayButtonAppearance(playResource);
+ break;
+ case SEEKING:
+ postStatusMsg(R.string.player_seeking_msg);
+ break;
+ case AWAITING_VIDEO_SURFACE:
+ onAwaitingVideoSurface();
+ break;
+ case INITIALIZED:
+ checkMediaInfoLoaded();
+ clearStatusMsg();
+ updatePlayButtonAppearance(playResource);
+ break;
+ }
+ }
+
+ private void checkMediaInfoLoaded() {
+ if (!mediaInfoLoaded) {
+ loadMediaInfo();
+ }
+ mediaInfoLoaded = true;
+ }
+
+ private void updatePlayButtonAppearance(int resource) {
+ ImageButton butPlay = getPlayButton();
+ butPlay.setImageResource(resource);
+ }
+
+ public abstract ImageButton getPlayButton();
+
+ public abstract void postStatusMsg(int msg);
+
+ public abstract void clearStatusMsg();
+
+ public abstract void loadMediaInfo();
+
+ public abstract void onAwaitingVideoSurface();
+
+ /**
+ * Called when connection to playback service has been established or
+ * information has to be refreshed
+ */
+ void queryService() {
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Querying service info");
+ if (playbackService != null) {
+ status = playbackService.getStatus();
+ media = playbackService.getMedia();
+ if (media == null) {
+ Log.w(TAG,
+ "PlaybackService has no media object. Trying to restore last played media.");
+ Intent serviceIntent = getPlayLastPlayedMediaIntent();
+ if (serviceIntent != null) {
+ activity.startService(serviceIntent);
+ }
+ }
+ onServiceQueried();
+
+ setupGUI();
+ handleStatus();
+ // make sure that new media is loaded if it's available
+ mediaInfoLoaded = false;
+
+ } else {
+ Log.e(TAG,
+ "queryService() was called without an existing connection to playbackservice");
+ }
+ }
+
+ public abstract void onServiceQueried();
+
+ /**
+ * Should be used by classes which implement the OnSeekBarChanged interface.
+ */
+ public float onSeekBarProgressChanged(SeekBar seekBar, int progress,
+ boolean fromUser, TextView txtvPosition) {
+ if (fromUser && playbackService != null) {
+ float prog = progress / ((float) seekBar.getMax());
+ int duration = media.getDuration();
+ txtvPosition.setText(Converter
+ .getDurationStringLong((int) (prog * duration)));
+ return prog;
+ }
+ return 0;
+
+ }
+
+ /**
+ * Should be used by classes which implement the OnSeekBarChanged interface.
+ */
+ public void onSeekBarStartTrackingTouch(SeekBar seekBar) {
+ // interrupt position Observer, restart later
+ cancelPositionObserver();
+ }
+
+ /**
+ * Should be used by classes which implement the OnSeekBarChanged interface.
+ */
+ public void onSeekBarStopTrackingTouch(SeekBar seekBar, float prog) {
+ if (playbackService != null) {
+ playbackService.seek((int) (prog * media.getDuration()));
+ setupPositionObserver();
+ }
+ }
+
+ public OnClickListener newOnPlayButtonClickListener() {
+ return new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (playbackService != null) {
+ switch (status) {
+ case PLAYING:
+ playbackService.pause(true, reinitOnPause);
+ break;
+ case PAUSED:
+ case PREPARED:
+ playbackService.play();
+ break;
+ case PREPARING:
+ playbackService.setStartWhenPrepared(!playbackService
+ .isStartWhenPrepared());
+ if (reinitOnPause
+ && playbackService.isStartWhenPrepared() == false) {
+ playbackService.reinit();
+ }
+ break;
+ case INITIALIZED:
+ playbackService.setStartWhenPrepared(true);
+ playbackService.prepare();
+ break;
+ }
+ } else {
+ Log.w(TAG,
+ "Play/Pause button was pressed, but playbackservice was null!");
+ }
+ }
+
+ };
+ }
+
+ public OnClickListener newOnRevButtonClickListener() {
+ return new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (status == PlayerStatus.PLAYING) {
+ playbackService.seekDelta(-DEFAULT_SEEK_DELTA);
+ }
+ }
+ };
+ }
+
+ public OnClickListener newOnFFButtonClickListener() {
+ return new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (status == PlayerStatus.PLAYING) {
+ playbackService.seekDelta(DEFAULT_SEEK_DELTA);
+ }
+ }
+ };
+ }
+
+ public boolean serviceAvailable() {
+ return playbackService != null;
+ }
+
+ public int getPosition() {
+ if (playbackService != null) {
+ return playbackService.getCurrentPositionSafe();
+ } else {
+ return PlaybackService.INVALID_TIME;
+ }
+ }
+
+ public int getDuration() {
+ if (playbackService != null) {
+ return playbackService.getDurationSafe();
+ } else {
+ return PlaybackService.INVALID_TIME;
+ }
+ }
+
+ public Playable getMedia() {
+ return media;
+ }
+
+ public boolean sleepTimerActive() {
+ return playbackService != null && playbackService.sleepTimerActive();
+ }
+
+ public boolean sleepTimerNotActive() {
+ return playbackService != null && !playbackService.sleepTimerActive();
+ }
+
+ public void disableSleepTimer() {
+ if (playbackService != null) {
+ playbackService.disableSleepTimer();
+ }
+ }
+
+ public long getSleepTimerTimeLeft() {
+ if (playbackService != null) {
+ return playbackService.getSleepTimerTimeLeft();
+ } else {
+ return INVALID_TIME;
+ }
+ }
+
+ public void setSleepTimer(long time) {
+ if (playbackService != null) {
+ playbackService.setSleepTimer(time);
+ }
+ }
+
+ public void seekToChapter(Chapter chapter) {
+ if (playbackService != null) {
+ playbackService.seekToChapter(chapter);
+ }
+ }
+
+ public void setVideoSurface(SurfaceHolder holder) {
+ if (playbackService != null) {
+ playbackService.setVideoSurface(holder);
+ }
+ }
+
+ public PlayerStatus getStatus() {
+ return status;
+ }
+
+ public boolean isPlayingVideo() {
+ if (playbackService != null) {
+ return PlaybackService.isPlayingVideo();
+ }
+ return false;
+ }
+
+ public void notifyVideoSurfaceAbandoned() {
+ if (playbackService != null) {
+ playbackService.notifyVideoSurfaceAbandoned();
+ }
+ }
+
+ /** Move service into INITIALIZED state if it's paused to save bandwidth */
+ public void reinitServiceIfPaused() {
+ if (playbackService != null
+ && playbackService.isShouldStream()
+ && (playbackService.getStatus() == PlayerStatus.PAUSED || (playbackService
+ .getStatus() == PlayerStatus.PREPARING && playbackService
+ .isStartWhenPrepared() == false))) {
+ playbackService.reinit();
+ }
+ }
+
+ /** Refreshes the current position of the media file that is playing. */
+ public class MediaPositionObserver implements Runnable {
+
+ public static final int WAITING_INTERVALL = 1000;
+
+ @Override
+ public void run() {
+ if (playbackService != null && playbackService.getPlayer() != null
+ && playbackService.getPlayer().isPlaying()) {
+ activity.runOnUiThread(new Runnable() {
+
+ @Override
+ public void run() {
+ onPositionObserverUpdate();
+ }
+ });
+ }
+ }
+ }
+}