From cc741d66434f72d22c2952785daff28369d0ef9b Mon Sep 17 00:00:00 2001 From: daniel oeh Date: Wed, 27 Feb 2013 11:53:17 +0100 Subject: Created de.danoeh.antennapod.util.playback package --- .../antennapod/activity/AudioplayerActivity.java | 2 +- .../antennapod/activity/MediaplayerActivity.java | 6 +- .../antennapod/activity/VideoplayerActivity.java | 2 +- .../antennapod/adapter/ChapterListAdapter.java | 2 +- src/de/danoeh/antennapod/feed/FeedMedia.java | 2 +- .../danoeh/antennapod/fragment/CoverFragment.java | 2 +- .../fragment/ExternalPlayerFragment.java | 4 +- .../fragment/ItemDescriptionFragment.java | 2 +- .../danoeh/antennapod/service/PlaybackService.java | 4 +- .../antennapod/service/PlayerWidgetService.java | 2 +- src/de/danoeh/antennapod/util/ChapterUtils.java | 1 + .../danoeh/antennapod/util/MediaPlayerError.java | 23 - src/de/danoeh/antennapod/util/Playable.java | 183 ------ .../danoeh/antennapod/util/PlaybackController.java | 678 -------------------- .../antennapod/util/playback/MediaPlayerError.java | 23 + .../danoeh/antennapod/util/playback/Playable.java | 183 ++++++ .../util/playback/PlaybackController.java | 679 +++++++++++++++++++++ 17 files changed, 900 insertions(+), 898 deletions(-) delete mode 100644 src/de/danoeh/antennapod/util/MediaPlayerError.java delete mode 100644 src/de/danoeh/antennapod/util/Playable.java delete mode 100644 src/de/danoeh/antennapod/util/PlaybackController.java create mode 100644 src/de/danoeh/antennapod/util/playback/MediaPlayerError.java create mode 100644 src/de/danoeh/antennapod/util/playback/Playable.java create mode 100644 src/de/danoeh/antennapod/util/playback/PlaybackController.java (limited to 'src') diff --git a/src/de/danoeh/antennapod/activity/AudioplayerActivity.java b/src/de/danoeh/antennapod/activity/AudioplayerActivity.java index 469acc9fb..c6fa5c79f 100644 --- a/src/de/danoeh/antennapod/activity/AudioplayerActivity.java +++ b/src/de/danoeh/antennapod/activity/AudioplayerActivity.java @@ -26,7 +26,7 @@ import de.danoeh.antennapod.feed.SimpleChapter; import de.danoeh.antennapod.fragment.CoverFragment; import de.danoeh.antennapod.fragment.ItemDescriptionFragment; import de.danoeh.antennapod.service.PlaybackService; -import de.danoeh.antennapod.util.Playable; +import de.danoeh.antennapod.util.playback.Playable; /** Activity for playing audio files. */ public class AudioplayerActivity extends MediaplayerActivity { diff --git a/src/de/danoeh/antennapod/activity/MediaplayerActivity.java b/src/de/danoeh/antennapod/activity/MediaplayerActivity.java index 09d3c5a3f..89a38466b 100644 --- a/src/de/danoeh/antennapod/activity/MediaplayerActivity.java +++ b/src/de/danoeh/antennapod/activity/MediaplayerActivity.java @@ -26,11 +26,11 @@ import de.danoeh.antennapod.feed.FeedManager; import de.danoeh.antennapod.preferences.UserPreferences; import de.danoeh.antennapod.service.PlaybackService; import de.danoeh.antennapod.util.Converter; -import de.danoeh.antennapod.util.MediaPlayerError; -import de.danoeh.antennapod.util.Playable; -import de.danoeh.antennapod.util.PlaybackController; import de.danoeh.antennapod.util.ShareUtils; import de.danoeh.antennapod.util.StorageUtils; +import de.danoeh.antennapod.util.playback.MediaPlayerError; +import de.danoeh.antennapod.util.playback.Playable; +import de.danoeh.antennapod.util.playback.PlaybackController; /** * Provides general features which are both needed for playing audio and video diff --git a/src/de/danoeh/antennapod/activity/VideoplayerActivity.java b/src/de/danoeh/antennapod/activity/VideoplayerActivity.java index 004ab85ab..a663d1012 100644 --- a/src/de/danoeh/antennapod/activity/VideoplayerActivity.java +++ b/src/de/danoeh/antennapod/activity/VideoplayerActivity.java @@ -22,7 +22,7 @@ import de.danoeh.antennapod.R; import de.danoeh.antennapod.preferences.UserPreferences; import de.danoeh.antennapod.service.PlaybackService; import de.danoeh.antennapod.service.PlayerStatus; -import de.danoeh.antennapod.util.Playable; +import de.danoeh.antennapod.util.playback.Playable; /** Activity for playing audio files. */ public class VideoplayerActivity extends MediaplayerActivity implements diff --git a/src/de/danoeh/antennapod/adapter/ChapterListAdapter.java b/src/de/danoeh/antennapod/adapter/ChapterListAdapter.java index e60b28189..3e9b586ce 100644 --- a/src/de/danoeh/antennapod/adapter/ChapterListAdapter.java +++ b/src/de/danoeh/antennapod/adapter/ChapterListAdapter.java @@ -22,7 +22,7 @@ import de.danoeh.antennapod.R; import de.danoeh.antennapod.feed.Chapter; import de.danoeh.antennapod.util.ChapterUtils; import de.danoeh.antennapod.util.Converter; -import de.danoeh.antennapod.util.Playable; +import de.danoeh.antennapod.util.playback.Playable; public class ChapterListAdapter extends ArrayAdapter { diff --git a/src/de/danoeh/antennapod/feed/FeedMedia.java b/src/de/danoeh/antennapod/feed/FeedMedia.java index 5d139f01a..40b220fd9 100644 --- a/src/de/danoeh/antennapod/feed/FeedMedia.java +++ b/src/de/danoeh/antennapod/feed/FeedMedia.java @@ -9,7 +9,7 @@ import android.os.Parcel; import android.os.Parcelable; import de.danoeh.antennapod.PodcastApp; import de.danoeh.antennapod.util.ChapterUtils; -import de.danoeh.antennapod.util.Playable; +import de.danoeh.antennapod.util.playback.Playable; public class FeedMedia extends FeedFile implements Playable { diff --git a/src/de/danoeh/antennapod/fragment/CoverFragment.java b/src/de/danoeh/antennapod/fragment/CoverFragment.java index 29a160960..c477ea2b8 100644 --- a/src/de/danoeh/antennapod/fragment/CoverFragment.java +++ b/src/de/danoeh/antennapod/fragment/CoverFragment.java @@ -13,7 +13,7 @@ import de.danoeh.antennapod.AppConfig; import de.danoeh.antennapod.R; import de.danoeh.antennapod.activity.AudioplayerActivity.AudioplayerContentFragment; import de.danoeh.antennapod.asynctask.ImageLoader; -import de.danoeh.antennapod.util.Playable; +import de.danoeh.antennapod.util.playback.Playable; /** Displays the cover and the title of a FeedItem. */ public class CoverFragment extends SherlockFragment implements diff --git a/src/de/danoeh/antennapod/fragment/ExternalPlayerFragment.java b/src/de/danoeh/antennapod/fragment/ExternalPlayerFragment.java index bc4f7bdec..94aa2b0fc 100644 --- a/src/de/danoeh/antennapod/fragment/ExternalPlayerFragment.java +++ b/src/de/danoeh/antennapod/fragment/ExternalPlayerFragment.java @@ -17,8 +17,8 @@ import de.danoeh.antennapod.R; import de.danoeh.antennapod.asynctask.ImageLoader; import de.danoeh.antennapod.service.PlaybackService; import de.danoeh.antennapod.util.Converter; -import de.danoeh.antennapod.util.Playable; -import de.danoeh.antennapod.util.PlaybackController; +import de.danoeh.antennapod.util.playback.Playable; +import de.danoeh.antennapod.util.playback.PlaybackController; /** * Fragment which is supposed to be displayed outside of the MediaplayerActivity diff --git a/src/de/danoeh/antennapod/fragment/ItemDescriptionFragment.java b/src/de/danoeh/antennapod/fragment/ItemDescriptionFragment.java index 5b1edd777..5fd563509 100644 --- a/src/de/danoeh/antennapod/fragment/ItemDescriptionFragment.java +++ b/src/de/danoeh/antennapod/fragment/ItemDescriptionFragment.java @@ -33,8 +33,8 @@ import de.danoeh.antennapod.R; import de.danoeh.antennapod.feed.FeedItem; import de.danoeh.antennapod.feed.FeedManager; import de.danoeh.antennapod.preferences.UserPreferences; -import de.danoeh.antennapod.util.Playable; import de.danoeh.antennapod.util.ShareUtils; +import de.danoeh.antennapod.util.playback.Playable; /** Displays the description of a Playable object in a Webview. */ public class ItemDescriptionFragment extends SherlockFragment { diff --git a/src/de/danoeh/antennapod/service/PlaybackService.java b/src/de/danoeh/antennapod/service/PlaybackService.java index 7068a35d2..7e2158297 100644 --- a/src/de/danoeh/antennapod/service/PlaybackService.java +++ b/src/de/danoeh/antennapod/service/PlaybackService.java @@ -52,9 +52,9 @@ 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.Playable; -import de.danoeh.antennapod.util.Playable.PlayableException; 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 { diff --git a/src/de/danoeh/antennapod/service/PlayerWidgetService.java b/src/de/danoeh/antennapod/service/PlayerWidgetService.java index 1a33806ba..61bfb4887 100644 --- a/src/de/danoeh/antennapod/service/PlayerWidgetService.java +++ b/src/de/danoeh/antennapod/service/PlayerWidgetService.java @@ -16,7 +16,7 @@ import de.danoeh.antennapod.R; import de.danoeh.antennapod.receiver.MediaButtonReceiver; import de.danoeh.antennapod.receiver.PlayerWidget; import de.danoeh.antennapod.util.Converter; -import de.danoeh.antennapod.util.Playable; +import de.danoeh.antennapod.util.playback.Playable; /** Updates the state of the player widget */ public class PlayerWidgetService extends Service { diff --git a/src/de/danoeh/antennapod/util/ChapterUtils.java b/src/de/danoeh/antennapod/util/ChapterUtils.java index 5590707dc..1c2210481 100644 --- a/src/de/danoeh/antennapod/util/ChapterUtils.java +++ b/src/de/danoeh/antennapod/util/ChapterUtils.java @@ -19,6 +19,7 @@ import de.danoeh.antennapod.feed.Chapter; import de.danoeh.antennapod.util.comparator.ChapterStartTimeComparator; import de.danoeh.antennapod.util.id3reader.ChapterReader; import de.danoeh.antennapod.util.id3reader.ID3ReaderException; +import de.danoeh.antennapod.util.playback.Playable; import de.danoeh.antennapod.util.vorbiscommentreader.VorbisCommentChapterReader; import de.danoeh.antennapod.util.vorbiscommentreader.VorbisCommentReaderException; diff --git a/src/de/danoeh/antennapod/util/MediaPlayerError.java b/src/de/danoeh/antennapod/util/MediaPlayerError.java deleted file mode 100644 index 93922c747..000000000 --- a/src/de/danoeh/antennapod/util/MediaPlayerError.java +++ /dev/null @@ -1,23 +0,0 @@ -package de.danoeh.antennapod.util; - -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/Playable.java b/src/de/danoeh/antennapod/util/Playable.java deleted file mode 100644 index 360a31ac1..000000000 --- a/src/de/danoeh/antennapod/util/Playable.java +++ /dev/null @@ -1,183 +0,0 @@ -package de.danoeh.antennapod.util; - -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 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. */ - public MediaType getMediaType(); - - /** - * Returns an url to a local file that can be played or null if this file - * does not exist. - */ - public String getFileUrl(); - - /** - * 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 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; - } - 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/PlaybackController.java b/src/de/danoeh/antennapod/util/PlaybackController.java deleted file mode 100644 index cad5af92e..000000000 --- a/src/de/danoeh/antennapod/util/PlaybackController.java +++ /dev/null @@ -1,678 +0,0 @@ -package de.danoeh.antennapod.util; - -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.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(); - } - }); - } - } - } -} 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..67acbb692 --- /dev/null +++ b/src/de/danoeh/antennapod/util/playback/Playable.java @@ -0,0 +1,183 @@ +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 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. */ + public MediaType getMediaType(); + + /** + * Returns an url to a local file that can be played or null if this file + * does not exist. + */ + public String getFileUrl(); + + /** + * 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 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; + } + 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(); + } + }); + } + } + } +} -- cgit v1.2.3