diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 5ed5d7a81..6d2eebf1b 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -153,7 +153,7 @@
android:name=".service.download.DownloadService"
android:enabled="true"/>
@@ -194,7 +194,7 @@
diff --git a/res/drawable-hdpi/ic_action_pause_over_video.png b/res/drawable-hdpi/ic_action_pause_over_video.png
new file mode 100755
index 000000000..64b07728f
Binary files /dev/null and b/res/drawable-hdpi/ic_action_pause_over_video.png differ
diff --git a/res/drawable-hdpi/ic_action_play_over_video.png b/res/drawable-hdpi/ic_action_play_over_video.png
new file mode 100755
index 000000000..a364ca7c2
Binary files /dev/null and b/res/drawable-hdpi/ic_action_play_over_video.png differ
diff --git a/res/drawable-mdpi/ic_action_pause_over_video.png b/res/drawable-mdpi/ic_action_pause_over_video.png
new file mode 100755
index 000000000..f478ac321
Binary files /dev/null and b/res/drawable-mdpi/ic_action_pause_over_video.png differ
diff --git a/res/drawable-mdpi/ic_action_play_over_video.png b/res/drawable-mdpi/ic_action_play_over_video.png
new file mode 100755
index 000000000..835ff3636
Binary files /dev/null and b/res/drawable-mdpi/ic_action_play_over_video.png differ
diff --git a/res/drawable-xhdpi/ic_action_pause_over_video.png b/res/drawable-xhdpi/ic_action_pause_over_video.png
new file mode 100755
index 000000000..b0777a023
Binary files /dev/null and b/res/drawable-xhdpi/ic_action_pause_over_video.png differ
diff --git a/res/drawable-xhdpi/ic_action_play_over_video.png b/res/drawable-xhdpi/ic_action_play_over_video.png
new file mode 100755
index 000000000..24331a48c
Binary files /dev/null and b/res/drawable-xhdpi/ic_action_play_over_video.png differ
diff --git a/res/drawable-xxhdpi/ic_action_pause_over_video.png b/res/drawable-xxhdpi/ic_action_pause_over_video.png
new file mode 100755
index 000000000..fa85601cf
Binary files /dev/null and b/res/drawable-xxhdpi/ic_action_pause_over_video.png differ
diff --git a/res/drawable-xxhdpi/ic_action_play_over_video.png b/res/drawable-xxhdpi/ic_action_play_over_video.png
new file mode 100755
index 000000000..121be211e
Binary files /dev/null and b/res/drawable-xxhdpi/ic_action_play_over_video.png differ
diff --git a/res/drawable/overlay_button_circle_background.xml b/res/drawable/overlay_button_circle_background.xml
new file mode 100644
index 000000000..90c51472c
--- /dev/null
+++ b/res/drawable/overlay_button_circle_background.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/res/layout-land/videoplayer_activity.xml b/res/layout-land/videoplayer_activity.xml
index 675d9709d..13d075b1c 100644
--- a/res/layout-land/videoplayer_activity.xml
+++ b/res/layout-land/videoplayer_activity.xml
@@ -1,13 +1,14 @@
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
-
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"/>
-
+ android:indeterminateOnly="true"/>
+
+
-
-
-
-
-
-
-
-
-
+ android:background="#80000000"
+ android:orientation="vertical">
+ android:layout_height="50dp"
+ android:paddingTop="8dp"
+ android:layout_marginBottom="4dp">
+ android:layout_marginTop="4dp"
+ android:textColor="@color/white"
+ android:textStyle="bold"
+ android:text="@string/position_default_label"/>
+ android:layout_marginTop="4dp"
+ android:textColor="@color/white"
+ android:textStyle="bold"
+ android:text="@string/position_default_label"/>
+ android:max="500"/>
diff --git a/src/de/danoeh/antennapod/activity/AudioplayerActivity.java b/src/de/danoeh/antennapod/activity/AudioplayerActivity.java
index db4373036..dc79bd3da 100644
--- a/src/de/danoeh/antennapod/activity/AudioplayerActivity.java
+++ b/src/de/danoeh/antennapod/activity/AudioplayerActivity.java
@@ -11,15 +11,10 @@ import android.support.v4.app.ListFragment;
import android.util.Log;
import android.view.View;
import android.view.View.OnClickListener;
-import android.view.Window;
import android.view.View.OnLongClickListener;
-import android.widget.ArrayAdapter;
-import android.widget.Button;
-import android.widget.ImageButton;
+import android.view.Window;
+import android.widget.*;
import android.widget.ImageView.ScaleType;
-import android.widget.ListView;
-import android.widget.TextView;
-
import de.danoeh.antennapod.AppConfig;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.adapter.ChapterListAdapter;
@@ -31,501 +26,511 @@ import de.danoeh.antennapod.feed.SimpleChapter;
import de.danoeh.antennapod.fragment.CoverFragment;
import de.danoeh.antennapod.fragment.ItemDescriptionFragment;
import de.danoeh.antennapod.preferences.UserPreferences;
-import de.danoeh.antennapod.preferences.UserPreferences;
-import de.danoeh.antennapod.service.PlaybackService;
+import de.danoeh.antennapod.service.playback.PlaybackService;
import de.danoeh.antennapod.util.playback.ExternalMedia;
import de.danoeh.antennapod.util.playback.Playable;
-/** Activity for playing audio files. */
+/**
+ * Activity for playing audio files.
+ */
public class AudioplayerActivity extends MediaplayerActivity {
- private static final int POS_COVER = 0;
- private static final int POS_DESCR = 1;
- private static final int POS_CHAPTERS = 2;
- private static final int NUM_CONTENT_FRAGMENTS = 3;
+ private static final int POS_COVER = 0;
+ private static final int POS_DESCR = 1;
+ private static final int POS_CHAPTERS = 2;
+ private static final int NUM_CONTENT_FRAGMENTS = 3;
- final String TAG = "AudioplayerActivity";
- private static final String PREFS = "AudioPlayerActivityPreferences";
- private static final String PREF_KEY_SELECTED_FRAGMENT_POSITION = "selectedFragmentPosition";
- private static final String PREF_PLAYABLE_ID = "playableId";
+ final String TAG = "AudioplayerActivity";
+ private static final String PREFS = "AudioPlayerActivityPreferences";
+ private static final String PREF_KEY_SELECTED_FRAGMENT_POSITION = "selectedFragmentPosition";
+ private static final String PREF_PLAYABLE_ID = "playableId";
- private Fragment[] detachedFragments;
+ private Fragment[] detachedFragments;
- private CoverFragment coverFragment;
- private ItemDescriptionFragment descriptionFragment;
- private ListFragment chapterFragment;
+ private CoverFragment coverFragment;
+ private ItemDescriptionFragment descriptionFragment;
+ private ListFragment chapterFragment;
- private Fragment currentlyShownFragment;
- private int currentlyShownPosition = -1;
- /** Used if onResume was called without loadMediaInfo. */
- private int savedPosition = -1;
+ private Fragment currentlyShownFragment;
+ private int currentlyShownPosition = -1;
+ /**
+ * Used if onResume was called without loadMediaInfo.
+ */
+ private int savedPosition = -1;
- private TextView txtvTitle;
- private TextView txtvFeed;
- private Button butPlaybackSpeed;
- private ImageButton butNavLeft;
- private ImageButton butNavRight;
+ private TextView txtvTitle;
+ private TextView txtvFeed;
+ private Button butPlaybackSpeed;
+ private ImageButton butNavLeft;
+ private ImageButton butNavRight;
- private void resetFragmentView() {
- FragmentTransaction fT = getSupportFragmentManager().beginTransaction();
+ private void resetFragmentView() {
+ FragmentTransaction fT = getSupportFragmentManager().beginTransaction();
- if (coverFragment != null) {
- if (AppConfig.DEBUG)
- Log.d(TAG, "Removing cover fragment");
- fT.remove(coverFragment);
- }
- if (descriptionFragment != null) {
- if (AppConfig.DEBUG)
- Log.d(TAG, "Removing description fragment");
- fT.remove(descriptionFragment);
- }
- if (chapterFragment != null) {
- if (AppConfig.DEBUG)
- Log.d(TAG, "Removing chapter fragment");
- fT.remove(chapterFragment);
- }
- if (currentlyShownFragment != null) {
- if (AppConfig.DEBUG)
- Log.d(TAG, "Removing currently shown fragment");
- fT.remove(currentlyShownFragment);
- }
- for (int i = 0; i < detachedFragments.length; i++) {
- Fragment f = detachedFragments[i];
- if (f != null) {
- if (AppConfig.DEBUG)
- Log.d(TAG, "Removing detached fragment");
- fT.remove(f);
- }
- }
- fT.commit();
- currentlyShownFragment = null;
- coverFragment = null;
- descriptionFragment = null;
- chapterFragment = null;
- currentlyShownPosition = -1;
- detachedFragments = new Fragment[NUM_CONTENT_FRAGMENTS];
- }
+ if (coverFragment != null) {
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Removing cover fragment");
+ fT.remove(coverFragment);
+ }
+ if (descriptionFragment != null) {
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Removing description fragment");
+ fT.remove(descriptionFragment);
+ }
+ if (chapterFragment != null) {
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Removing chapter fragment");
+ fT.remove(chapterFragment);
+ }
+ if (currentlyShownFragment != null) {
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Removing currently shown fragment");
+ fT.remove(currentlyShownFragment);
+ }
+ for (int i = 0; i < detachedFragments.length; i++) {
+ Fragment f = detachedFragments[i];
+ if (f != null) {
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Removing detached fragment");
+ fT.remove(f);
+ }
+ }
+ fT.commit();
+ currentlyShownFragment = null;
+ coverFragment = null;
+ descriptionFragment = null;
+ chapterFragment = null;
+ currentlyShownPosition = -1;
+ detachedFragments = new Fragment[NUM_CONTENT_FRAGMENTS];
+ }
- @Override
- protected void onStop() {
- super.onStop();
- if (AppConfig.DEBUG)
- Log.d(TAG, "onStop");
+ @Override
+ protected void onStop() {
+ super.onStop();
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "onStop");
- }
+ }
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS);
- super.onCreate(savedInstanceState);
- getSupportActionBar().setDisplayShowTitleEnabled(false);
- detachedFragments = new Fragment[NUM_CONTENT_FRAGMENTS];
- }
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS);
+ super.onCreate(savedInstanceState);
+ getSupportActionBar().setDisplayShowTitleEnabled(false);
+ detachedFragments = new Fragment[NUM_CONTENT_FRAGMENTS];
+ }
- private void savePreferences() {
- if (AppConfig.DEBUG)
- Log.d(TAG, "Saving preferences");
- SharedPreferences prefs = getSharedPreferences(PREFS, MODE_PRIVATE);
- SharedPreferences.Editor editor = prefs.edit();
- if (currentlyShownPosition >= 0 && controller != null
- && controller.getMedia() != null) {
- editor.putInt(PREF_KEY_SELECTED_FRAGMENT_POSITION,
- currentlyShownPosition);
- editor.putString(PREF_PLAYABLE_ID, controller.getMedia()
- .getIdentifier().toString());
- } else {
- editor.putInt(PREF_KEY_SELECTED_FRAGMENT_POSITION, -1);
- editor.putString(PREF_PLAYABLE_ID, "");
- }
- editor.commit();
+ private void savePreferences() {
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Saving preferences");
+ SharedPreferences prefs = getSharedPreferences(PREFS, MODE_PRIVATE);
+ SharedPreferences.Editor editor = prefs.edit();
+ if (currentlyShownPosition >= 0 && controller != null
+ && controller.getMedia() != null) {
+ editor.putInt(PREF_KEY_SELECTED_FRAGMENT_POSITION,
+ currentlyShownPosition);
+ editor.putString(PREF_PLAYABLE_ID, controller.getMedia()
+ .getIdentifier().toString());
+ } else {
+ editor.putInt(PREF_KEY_SELECTED_FRAGMENT_POSITION, -1);
+ editor.putString(PREF_PLAYABLE_ID, "");
+ }
+ editor.commit();
- savedPosition = currentlyShownPosition;
- }
+ savedPosition = currentlyShownPosition;
+ }
- @Override
- public void onConfigurationChanged(Configuration newConfig) {
- super.onConfigurationChanged(newConfig);
- }
+ @Override
+ public void onConfigurationChanged(Configuration newConfig) {
+ super.onConfigurationChanged(newConfig);
+ }
- @Override
- protected void onSaveInstanceState(Bundle outState) {
- // super.onSaveInstanceState(outState); would cause crash
- if (AppConfig.DEBUG)
- Log.d(TAG, "onSaveInstanceState");
- }
+ @Override
+ protected void onSaveInstanceState(Bundle outState) {
+ // super.onSaveInstanceState(outState); would cause crash
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "onSaveInstanceState");
+ }
- @Override
- protected void onPause() {
- savePreferences();
- resetFragmentView();
- super.onPause();
- }
+ @Override
+ protected void onPause() {
+ savePreferences();
+ resetFragmentView();
+ super.onPause();
+ }
- @Override
- protected void onRestoreInstanceState(Bundle savedInstanceState) {
- super.onRestoreInstanceState(savedInstanceState);
- restoreFromPreferences();
- }
+ @Override
+ protected void onRestoreInstanceState(Bundle savedInstanceState) {
+ super.onRestoreInstanceState(savedInstanceState);
+ restoreFromPreferences();
+ }
- /**
- * Tries to restore the selected fragment position from the Activity's
- * preferences.
- *
- * @return true if restoreFromPrefernces changed the activity's state
- * */
- private boolean restoreFromPreferences() {
- if (AppConfig.DEBUG)
- Log.d(TAG, "Restoring instance state");
- SharedPreferences prefs = getSharedPreferences(PREFS, MODE_PRIVATE);
- int savedPosition = prefs.getInt(PREF_KEY_SELECTED_FRAGMENT_POSITION,
- -1);
- String playableId = prefs.getString(PREF_PLAYABLE_ID, "");
+ /**
+ * Tries to restore the selected fragment position from the Activity's
+ * preferences.
+ *
+ * @return true if restoreFromPrefernces changed the activity's state
+ */
+ private boolean restoreFromPreferences() {
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Restoring instance state");
+ SharedPreferences prefs = getSharedPreferences(PREFS, MODE_PRIVATE);
+ int savedPosition = prefs.getInt(PREF_KEY_SELECTED_FRAGMENT_POSITION,
+ -1);
+ String playableId = prefs.getString(PREF_PLAYABLE_ID, "");
- if (savedPosition != -1
- && controller != null
- && controller.getMedia() != null
- && controller.getMedia().getIdentifier().toString()
- .equals(playableId)) {
- switchToFragment(savedPosition);
- return true;
- } else if (controller == null || controller.getMedia() == null) {
- if (AppConfig.DEBUG)
- Log.d(TAG,
- "Couldn't restore from preferences: controller or media was null");
- } else {
- if (AppConfig.DEBUG)
- Log.d(TAG,
- "Couldn't restore from preferences: savedPosition was -1 or saved identifier and playable identifier didn't match.\nsavedPosition: "
- + savedPosition + ", id: " + playableId);
+ if (savedPosition != -1
+ && controller != null
+ && controller.getMedia() != null
+ && controller.getMedia().getIdentifier().toString()
+ .equals(playableId)) {
+ switchToFragment(savedPosition);
+ return true;
+ } else if (controller == null || controller.getMedia() == null) {
+ if (AppConfig.DEBUG)
+ Log.d(TAG,
+ "Couldn't restore from preferences: controller or media was null");
+ } else {
+ if (AppConfig.DEBUG)
+ Log.d(TAG,
+ "Couldn't restore from preferences: savedPosition was -1 or saved identifier and playable identifier didn't match.\nsavedPosition: "
+ + savedPosition + ", id: " + playableId);
- }
- return false;
- }
+ }
+ return false;
+ }
- @Override
- protected void onResume() {
- super.onResume();
- if (getIntent().getAction() != null
- && getIntent().getAction().equals(Intent.ACTION_VIEW)) {
- Intent intent = getIntent();
- if (AppConfig.DEBUG)
- Log.d(TAG, "Received VIEW intent: "
- + intent.getData().getPath());
- ExternalMedia media = new ExternalMedia(intent.getData().getPath(),
- MediaType.AUDIO);
- Intent launchIntent = new Intent(this, PlaybackService.class);
- launchIntent.putExtra(PlaybackService.EXTRA_PLAYABLE, media);
- launchIntent.putExtra(PlaybackService.EXTRA_START_WHEN_PREPARED,
- true);
- launchIntent.putExtra(PlaybackService.EXTRA_SHOULD_STREAM, false);
- launchIntent.putExtra(PlaybackService.EXTRA_PREPARE_IMMEDIATELY,
- true);
- startService(launchIntent);
- }
- if (savedPosition != -1) {
- switchToFragment(savedPosition);
- }
+ @Override
+ protected void onResume() {
+ super.onResume();
+ if (getIntent().getAction() != null
+ && getIntent().getAction().equals(Intent.ACTION_VIEW)) {
+ Intent intent = getIntent();
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Received VIEW intent: "
+ + intent.getData().getPath());
+ ExternalMedia media = new ExternalMedia(intent.getData().getPath(),
+ MediaType.AUDIO);
+ Intent launchIntent = new Intent(this, PlaybackService.class);
+ launchIntent.putExtra(PlaybackService.EXTRA_PLAYABLE, media);
+ launchIntent.putExtra(PlaybackService.EXTRA_START_WHEN_PREPARED,
+ true);
+ launchIntent.putExtra(PlaybackService.EXTRA_SHOULD_STREAM, false);
+ launchIntent.putExtra(PlaybackService.EXTRA_PREPARE_IMMEDIATELY,
+ true);
+ startService(launchIntent);
+ }
+ if (savedPosition != -1) {
+ switchToFragment(savedPosition);
+ }
- }
+ }
- @Override
- protected void onNewIntent(Intent intent) {
- super.onNewIntent(intent);
- setIntent(intent);
- }
+ @Override
+ protected void onNewIntent(Intent intent) {
+ super.onNewIntent(intent);
+ setIntent(intent);
+ }
- @Override
- protected void onAwaitingVideoSurface() {
- startActivity(new Intent(this, VideoplayerActivity.class));
- }
+ @Override
+ protected void onAwaitingVideoSurface() {
+ if (AppConfig.DEBUG) Log.d(TAG, "onAwaitingVideoSurface was called in audio player -> switching to video player");
+ startActivity(new Intent(this, VideoplayerActivity.class));
+ }
- @Override
- protected void postStatusMsg(int resId) {
- setSupportProgressBarIndeterminateVisibility(resId == R.string.player_preparing_msg
- || resId == R.string.player_seeking_msg
- || resId == R.string.player_buffering_msg);
- }
+ @Override
+ protected void postStatusMsg(int resId) {
+ setSupportProgressBarIndeterminateVisibility(resId == R.string.player_preparing_msg
+ || resId == R.string.player_seeking_msg
+ || resId == R.string.player_buffering_msg);
+ }
- @Override
- protected void clearStatusMsg() {
- setSupportProgressBarIndeterminateVisibility(false);
- }
+ @Override
+ protected void clearStatusMsg() {
+ setSupportProgressBarIndeterminateVisibility(false);
+ }
- /**
- * Changes the currently displayed fragment.
- *
- * @param pos Must be POS_COVER, POS_DESCR, or POS_CHAPTERS
- * */
- private void switchToFragment(int pos) {
- if (AppConfig.DEBUG)
- Log.d(TAG, "Switching contentView to position " + pos);
- if (currentlyShownPosition != pos && controller != null) {
- Playable media = controller.getMedia();
- if (media != null) {
- FragmentTransaction ft = getSupportFragmentManager()
- .beginTransaction();
- if (currentlyShownFragment != null) {
- detachedFragments[currentlyShownPosition] = currentlyShownFragment;
- ft.detach(currentlyShownFragment);
- }
- switch (pos) {
- case POS_COVER:
- if (coverFragment == null) {
- Log.i(TAG, "Using new coverfragment");
- coverFragment = CoverFragment.newInstance(media);
- }
- currentlyShownFragment = coverFragment;
- break;
- case POS_DESCR:
- if (descriptionFragment == null) {
- descriptionFragment = ItemDescriptionFragment
- .newInstance(media, true);
- }
- currentlyShownFragment = descriptionFragment;
- break;
- case POS_CHAPTERS:
- if (chapterFragment == null) {
- chapterFragment = new ListFragment() {
+ /**
+ * Changes the currently displayed fragment.
+ *
+ * @param pos Must be POS_COVER, POS_DESCR, or POS_CHAPTERS
+ */
+ private void switchToFragment(int pos) {
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Switching contentView to position " + pos);
+ if (currentlyShownPosition != pos && controller != null) {
+ Playable media = controller.getMedia();
+ if (media != null) {
+ FragmentTransaction ft = getSupportFragmentManager()
+ .beginTransaction();
+ if (currentlyShownFragment != null) {
+ detachedFragments[currentlyShownPosition] = currentlyShownFragment;
+ ft.detach(currentlyShownFragment);
+ }
+ switch (pos) {
+ case POS_COVER:
+ if (coverFragment == null) {
+ Log.i(TAG, "Using new coverfragment");
+ coverFragment = CoverFragment.newInstance(media);
+ }
+ currentlyShownFragment = coverFragment;
+ break;
+ case POS_DESCR:
+ if (descriptionFragment == null) {
+ descriptionFragment = ItemDescriptionFragment
+ .newInstance(media, true);
+ }
+ currentlyShownFragment = descriptionFragment;
+ break;
+ case POS_CHAPTERS:
+ if (chapterFragment == null) {
+ chapterFragment = new ListFragment() {
- @Override
- public void onListItemClick(ListView l, View v,
- int position, long id) {
- super.onListItemClick(l, v, position, id);
- Chapter chapter = (Chapter) this
- .getListAdapter().getItem(position);
- controller.seekToChapter(chapter);
- }
+ @Override
+ public void onListItemClick(ListView l, View v,
+ int position, long id) {
+ super.onListItemClick(l, v, position, id);
+ Chapter chapter = (Chapter) this
+ .getListAdapter().getItem(position);
+ controller.seekToChapter(chapter);
+ }
- };
- chapterFragment.setListAdapter(new ChapterListAdapter(
- AudioplayerActivity.this, 0, media
- .getChapters(), media));
- }
- currentlyShownFragment = chapterFragment;
- break;
- }
- if (currentlyShownFragment != null) {
- currentlyShownPosition = pos;
- if (detachedFragments[pos] != null) {
- if (AppConfig.DEBUG)
- Log.d(TAG, "Reattaching fragment at position "
- + pos);
- ft.attach(detachedFragments[pos]);
- } else {
- ft.add(R.id.contentView, currentlyShownFragment);
- }
- ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN);
- ft.disallowAddToBackStack();
- ft.commit();
- updateNavButtonDrawable();
- }
- }
- }
- }
+ };
+ chapterFragment.setListAdapter(new ChapterListAdapter(
+ AudioplayerActivity.this, 0, media
+ .getChapters(), media));
+ }
+ currentlyShownFragment = chapterFragment;
+ break;
+ }
+ if (currentlyShownFragment != null) {
+ currentlyShownPosition = pos;
+ if (detachedFragments[pos] != null) {
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Reattaching fragment at position "
+ + pos);
+ ft.attach(detachedFragments[pos]);
+ } else {
+ ft.add(R.id.contentView, currentlyShownFragment);
+ }
+ ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN);
+ ft.disallowAddToBackStack();
+ ft.commit();
+ updateNavButtonDrawable();
+ }
+ }
+ }
+ }
- private void updateNavButtonDrawable() {
- TypedArray drawables = obtainStyledAttributes(new int[] {
- R.attr.navigation_shownotes, R.attr.navigation_chapters });
- final Playable media = controller.getMedia();
- if (butNavLeft != null && butNavRight != null && media != null) {
- switch (currentlyShownPosition) {
- case POS_COVER:
- butNavLeft.setScaleType(ScaleType.CENTER);
- butNavLeft.setImageDrawable(drawables.getDrawable(0));
- butNavRight.setImageDrawable(drawables.getDrawable(1));
- break;
- case POS_DESCR:
- butNavLeft.setScaleType(ScaleType.CENTER_CROP);
- butNavLeft.post(new Runnable() {
+ private void updateNavButtonDrawable() {
+ TypedArray drawables = obtainStyledAttributes(new int[]{
+ R.attr.navigation_shownotes, R.attr.navigation_chapters});
+ final Playable media = controller.getMedia();
+ if (butNavLeft != null && butNavRight != null && media != null) {
+ switch (currentlyShownPosition) {
+ case POS_COVER:
+ butNavLeft.setScaleType(ScaleType.CENTER);
+ butNavLeft.setImageDrawable(drawables.getDrawable(0));
+ butNavRight.setImageDrawable(drawables.getDrawable(1));
+ break;
+ case POS_DESCR:
+ butNavLeft.setScaleType(ScaleType.CENTER_CROP);
+ butNavLeft.post(new Runnable() {
- @Override
- public void run() {
- ImageLoader.getInstance().loadThumbnailBitmap(media,
- butNavLeft);
- }
- });
- butNavRight.setImageDrawable(drawables.getDrawable(1));
- break;
- case POS_CHAPTERS:
- butNavLeft.setScaleType(ScaleType.CENTER_CROP);
- butNavLeft.post(new Runnable() {
+ @Override
+ public void run() {
+ ImageLoader.getInstance().loadThumbnailBitmap(media,
+ butNavLeft);
+ }
+ });
+ butNavRight.setImageDrawable(drawables.getDrawable(1));
+ break;
+ case POS_CHAPTERS:
+ butNavLeft.setScaleType(ScaleType.CENTER_CROP);
+ butNavLeft.post(new Runnable() {
- @Override
- public void run() {
- ImageLoader.getInstance().loadThumbnailBitmap(media,
- butNavLeft);
- }
- });
- butNavRight.setImageDrawable(drawables.getDrawable(0));
- break;
- }
- }
- }
+ @Override
+ public void run() {
+ ImageLoader.getInstance().loadThumbnailBitmap(media,
+ butNavLeft);
+ }
+ });
+ butNavRight.setImageDrawable(drawables.getDrawable(0));
+ break;
+ }
+ }
+ }
- @Override
- protected void setupGUI() {
- super.setupGUI();
- resetFragmentView();
- txtvTitle = (TextView) findViewById(R.id.txtvTitle);
- txtvFeed = (TextView) findViewById(R.id.txtvFeed);
- butNavLeft = (ImageButton) findViewById(R.id.butNavLeft);
- butNavRight = (ImageButton) findViewById(R.id.butNavRight);
- butPlaybackSpeed = (Button) findViewById(R.id.butPlaybackSpeed);
+ @Override
+ protected void setupGUI() {
+ super.setupGUI();
+ resetFragmentView();
+ txtvTitle = (TextView) findViewById(R.id.txtvTitle);
+ txtvFeed = (TextView) findViewById(R.id.txtvFeed);
+ butNavLeft = (ImageButton) findViewById(R.id.butNavLeft);
+ butNavRight = (ImageButton) findViewById(R.id.butNavRight);
+ butPlaybackSpeed = (Button) findViewById(R.id.butPlaybackSpeed);
- butNavLeft.setOnClickListener(new OnClickListener() {
-
- @Override
- public void onClick(View v) {
- if (currentlyShownFragment == null
- || currentlyShownPosition == POS_DESCR) {
- switchToFragment(POS_COVER);
- } else if (currentlyShownPosition == POS_COVER) {
- switchToFragment(POS_DESCR);
- } else if (currentlyShownPosition == POS_CHAPTERS) {
- switchToFragment(POS_COVER);
- }
- }
- });
-
- butNavRight.setOnClickListener(new OnClickListener() {
-
- @Override
- public void onClick(View v) {
- if (currentlyShownPosition == POS_CHAPTERS) {
- switchToFragment(POS_DESCR);
- } else {
- switchToFragment(POS_CHAPTERS);
- }
- }
- });
-
- butPlaybackSpeed.setOnClickListener(new OnClickListener() {
+ butNavLeft.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
- if (controller != null && controller.canSetPlaybackSpeed()) {
- String[] availableSpeeds = UserPreferences
- .getPlaybackSpeedArray();
- String currentSpeed = UserPreferences.getPlaybackSpeed();
-
- // Provide initial value in case the speed list has changed
- // out from under us
- // and our current speed isn't in the new list
- String newSpeed;
- if (availableSpeeds.length > 0) {
- newSpeed = availableSpeeds[0];
- } else {
- newSpeed = "1.0";
- }
-
- for (int i = 0; i < availableSpeeds.length; i++) {
- if (availableSpeeds[i].equals(currentSpeed)) {
- if (i == availableSpeeds.length - 1) {
- newSpeed = availableSpeeds[0];
- } else {
- newSpeed = availableSpeeds[i + 1];
- }
- break;
- }
- }
- UserPreferences.setPlaybackSpeed(newSpeed);
- controller.setPlaybackSpeed(Float.parseFloat(newSpeed));
+ if (currentlyShownFragment == null
+ || currentlyShownPosition == POS_DESCR) {
+ switchToFragment(POS_COVER);
+ } else if (currentlyShownPosition == POS_COVER) {
+ switchToFragment(POS_DESCR);
+ } else if (currentlyShownPosition == POS_CHAPTERS) {
+ switchToFragment(POS_COVER);
}
}
});
- butPlaybackSpeed.setOnLongClickListener(new OnLongClickListener() {
+ butNavRight.setOnClickListener(new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ if (currentlyShownPosition == POS_CHAPTERS) {
+ switchToFragment(POS_DESCR);
+ } else {
+ switchToFragment(POS_CHAPTERS);
+ }
+ }
+ });
+
+ butPlaybackSpeed.setOnClickListener(new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ if (controller != null && controller.canSetPlaybackSpeed()) {
+ String[] availableSpeeds = UserPreferences
+ .getPlaybackSpeedArray();
+ String currentSpeed = UserPreferences.getPlaybackSpeed();
+
+ // Provide initial value in case the speed list has changed
+ // out from under us
+ // and our current speed isn't in the new list
+ String newSpeed;
+ if (availableSpeeds.length > 0) {
+ newSpeed = availableSpeeds[0];
+ } else {
+ newSpeed = "1.0";
+ }
+
+ for (int i = 0; i < availableSpeeds.length; i++) {
+ if (availableSpeeds[i].equals(currentSpeed)) {
+ if (i == availableSpeeds.length - 1) {
+ newSpeed = availableSpeeds[0];
+ } else {
+ newSpeed = availableSpeeds[i + 1];
+ }
+ break;
+ }
+ }
+ UserPreferences.setPlaybackSpeed(newSpeed);
+ controller.setPlaybackSpeed(Float.parseFloat(newSpeed));
+ }
+ }
+ });
+
+ butPlaybackSpeed.setOnLongClickListener(new OnLongClickListener() {
@Override
public boolean onLongClick(View v) {
VariableSpeedDialog.showDialog(AudioplayerActivity.this);
return true;
}
});
- }
+ }
- @Override
- protected void onPlaybackSpeedChange() {
- super.onPlaybackSpeedChange();
- updateButPlaybackSpeed();
- }
+ @Override
+ protected void onPlaybackSpeedChange() {
+ super.onPlaybackSpeedChange();
+ updateButPlaybackSpeed();
+ }
- private void updateButPlaybackSpeed() {
- if (controller == null
- || (controller.getCurrentPlaybackSpeedMultiplier() == -1)) {
- butPlaybackSpeed.setVisibility(View.GONE);
- } else {
- butPlaybackSpeed.setVisibility(View.VISIBLE);
- butPlaybackSpeed.setText(UserPreferences.getPlaybackSpeed());
- }
- }
+ private void updateButPlaybackSpeed() {
+ if (controller == null
+ || (controller.getCurrentPlaybackSpeedMultiplier() == -1)) {
+ butPlaybackSpeed.setVisibility(View.GONE);
+ } else {
+ butPlaybackSpeed.setVisibility(View.VISIBLE);
+ butPlaybackSpeed.setText(UserPreferences.getPlaybackSpeed());
+ }
+ }
- @Override
- protected void onPositionObserverUpdate() {
- super.onPositionObserverUpdate();
- notifyMediaPositionChanged();
- }
+ @Override
+ protected void onPositionObserverUpdate() {
+ super.onPositionObserverUpdate();
+ notifyMediaPositionChanged();
+ }
- @Override
- protected void loadMediaInfo() {
- super.loadMediaInfo();
- final Playable media = controller.getMedia();
- if (media != null) {
- txtvTitle.setText(media.getEpisodeTitle());
- txtvFeed.setText(media.getFeedTitle());
- if (media.getChapters() != null) {
- butNavRight.setVisibility(View.VISIBLE);
- } else {
- butNavRight.setVisibility(View.GONE);
- }
+ @Override
+ protected boolean loadMediaInfo() {
+ if (!super.loadMediaInfo()) {
+ return false;
+ }
+ final Playable media = controller.getMedia();
+ if (media == null) {
+ return false;
+ }
+ txtvTitle.setText(media.getEpisodeTitle());
+ txtvFeed.setText(media.getFeedTitle());
+ if (media.getChapters() != null) {
+ butNavRight.setVisibility(View.VISIBLE);
+ } else {
+ butNavRight.setVisibility(View.GONE);
+ }
- }
- if (currentlyShownPosition == -1) {
- if (!restoreFromPreferences()) {
- switchToFragment(POS_COVER);
- }
- }
- if (currentlyShownFragment instanceof AudioplayerContentFragment) {
- ((AudioplayerContentFragment) currentlyShownFragment)
- .onDataSetChanged(media);
- }
- updateButPlaybackSpeed();
- }
- public void notifyMediaPositionChanged() {
- if (chapterFragment != null) {
- ArrayAdapter adapter = (ArrayAdapter) chapterFragment
- .getListAdapter();
- adapter.notifyDataSetChanged();
- }
- }
+ if (currentlyShownPosition == -1) {
+ if (!restoreFromPreferences()) {
+ switchToFragment(POS_COVER);
+ }
+ }
+ if (currentlyShownFragment instanceof AudioplayerContentFragment) {
+ ((AudioplayerContentFragment) currentlyShownFragment)
+ .onDataSetChanged(media);
+ }
+ updateButPlaybackSpeed();
+ return true;
+ }
- @Override
- protected void onReloadNotification(int notificationCode) {
- if (notificationCode == PlaybackService.EXTRA_CODE_VIDEO) {
- if (AppConfig.DEBUG)
- Log.d(TAG,
- "ReloadNotification received, switching to Videoplayer now");
- startActivity(new Intent(this, VideoplayerActivity.class));
+ public void notifyMediaPositionChanged() {
+ if (chapterFragment != null) {
+ ArrayAdapter adapter = (ArrayAdapter) chapterFragment
+ .getListAdapter();
+ adapter.notifyDataSetChanged();
+ }
+ }
- }
- }
+ @Override
+ protected void onReloadNotification(int notificationCode) {
+ if (notificationCode == PlaybackService.EXTRA_CODE_VIDEO) {
+ if (AppConfig.DEBUG)
+ Log.d(TAG,
+ "ReloadNotification received, switching to Videoplayer now");
+ finish();
+ startActivity(new Intent(this, VideoplayerActivity.class));
- @Override
- protected void onBufferStart() {
- postStatusMsg(R.string.player_buffering_msg);
- }
+ }
+ }
- @Override
- protected void onBufferEnd() {
- clearStatusMsg();
- }
+ @Override
+ protected void onBufferStart() {
+ postStatusMsg(R.string.player_buffering_msg);
+ }
- public interface AudioplayerContentFragment {
- public void onDataSetChanged(Playable media);
- }
+ @Override
+ protected void onBufferEnd() {
+ clearStatusMsg();
+ }
- @Override
- protected int getContentViewResourceId() {
- return R.layout.audioplayer_activity;
- }
+ public interface AudioplayerContentFragment {
+ public void onDataSetChanged(Playable media);
+ }
+
+ @Override
+ protected int getContentViewResourceId() {
+ return R.layout.audioplayer_activity;
+ }
}
diff --git a/src/de/danoeh/antennapod/activity/MainActivity.java b/src/de/danoeh/antennapod/activity/MainActivity.java
index f373bc35b..9edb312de 100644
--- a/src/de/danoeh/antennapod/activity/MainActivity.java
+++ b/src/de/danoeh/antennapod/activity/MainActivity.java
@@ -29,7 +29,7 @@ import de.danoeh.antennapod.fragment.EpisodesFragment;
import de.danoeh.antennapod.fragment.ExternalPlayerFragment;
import de.danoeh.antennapod.fragment.FeedlistFragment;
import de.danoeh.antennapod.preferences.UserPreferences;
-import de.danoeh.antennapod.service.PlaybackService;
+import de.danoeh.antennapod.service.playback.PlaybackService;
import de.danoeh.antennapod.service.download.DownloadService;
import de.danoeh.antennapod.storage.DBReader;
import de.danoeh.antennapod.storage.DBTasks;
diff --git a/src/de/danoeh/antennapod/activity/MediaplayerActivity.java b/src/de/danoeh/antennapod/activity/MediaplayerActivity.java
index 748a049a6..60589bdf5 100644
--- a/src/de/danoeh/antennapod/activity/MediaplayerActivity.java
+++ b/src/de/danoeh/antennapod/activity/MediaplayerActivity.java
@@ -22,7 +22,7 @@ import de.danoeh.antennapod.R;
import de.danoeh.antennapod.asynctask.FlattrClickWorker;
import de.danoeh.antennapod.dialog.TimeDialog;
import de.danoeh.antennapod.preferences.UserPreferences;
-import de.danoeh.antennapod.service.PlaybackService;
+import de.danoeh.antennapod.service.playback.PlaybackService;
import de.danoeh.antennapod.util.Converter;
import de.danoeh.antennapod.util.ShareUtils;
import de.danoeh.antennapod.util.StorageUtils;
@@ -106,8 +106,8 @@ public abstract class MediaplayerActivity extends ActionBarActivity
}
@Override
- public void loadMediaInfo() {
- MediaplayerActivity.this.loadMediaInfo();
+ public boolean loadMediaInfo() {
+ return MediaplayerActivity.this.loadMediaInfo();
}
@Override
@@ -146,9 +146,13 @@ public abstract class MediaplayerActivity extends ActionBarActivity
supportInvalidateOptionsMenu();
}
+ protected void chooseTheme() {
+ setTheme(UserPreferences.getTheme());
+ }
+
@Override
protected void onCreate(Bundle savedInstanceState) {
- setTheme(UserPreferences.getTheme());
+ chooseTheme();
super.onCreate(savedInstanceState);
if (AppConfig.DEBUG)
Log.d(TAG, "Creating Activity");
@@ -339,8 +343,9 @@ public abstract class MediaplayerActivity extends ActionBarActivity
}
/**
- * Called by 'handleStatus()' when the PlaybackService is in the
- * AWAITING_VIDEO_SURFACE state.
+ * Called by 'handleStatus()' when the PlaybackService is waiting for
+ * a video surface.
+ *
*/
protected abstract void onAwaitingVideoSurface();
@@ -380,7 +385,7 @@ public abstract class MediaplayerActivity extends ActionBarActivity
* to the PlaybackService to ensure that the activity has the right
* FeedMedia object.
*/
- protected void loadMediaInfo() {
+ protected boolean loadMediaInfo() {
if (AppConfig.DEBUG)
Log.d(TAG, "Loading media info");
Playable media = controller.getMedia();
@@ -395,7 +400,10 @@ public abstract class MediaplayerActivity extends ActionBarActivity
/ media.getDuration();
sbPosition.setProgress((int) (progress * sbPosition.getMax()));
}
- }
+ return true;
+ } else {
+ return false;
+ }
}
protected void setupGUI() {
@@ -415,9 +423,12 @@ public abstract class MediaplayerActivity extends ActionBarActivity
butPlay.setOnClickListener(controller.newOnPlayButtonClickListener());
- butFF.setOnClickListener(controller.newOnFFButtonClickListener());
-
- butRev.setOnClickListener(controller.newOnRevButtonClickListener());
+ if (butFF != null) {
+ butFF.setOnClickListener(controller.newOnFFButtonClickListener());
+ }
+ if (butRev != null) {
+ butRev.setOnClickListener(controller.newOnRevButtonClickListener());
+ }
}
diff --git a/src/de/danoeh/antennapod/activity/VideoplayerActivity.java b/src/de/danoeh/antennapod/activity/VideoplayerActivity.java
index 01841f099..f323cb681 100644
--- a/src/de/danoeh/antennapod/activity/VideoplayerActivity.java
+++ b/src/de/danoeh/antennapod/activity/VideoplayerActivity.java
@@ -2,289 +2,338 @@ package de.danoeh.antennapod.activity;
import android.annotation.SuppressLint;
import android.content.Intent;
+import android.graphics.drawable.ColorDrawable;
import android.os.AsyncTask;
+import android.os.Build;
import android.os.Bundle;
import android.util.Log;
+import android.util.Pair;
import android.view.*;
+import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import android.widget.LinearLayout;
import android.widget.ProgressBar;
import android.widget.SeekBar;
-import android.widget.VideoView;
-
import de.danoeh.antennapod.AppConfig;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.feed.MediaType;
-import de.danoeh.antennapod.preferences.UserPreferences;
-import de.danoeh.antennapod.service.PlaybackService;
-import de.danoeh.antennapod.service.PlayerStatus;
+import de.danoeh.antennapod.service.playback.PlaybackService;
+import de.danoeh.antennapod.service.playback.PlayerStatus;
import de.danoeh.antennapod.util.playback.ExternalMedia;
import de.danoeh.antennapod.util.playback.Playable;
+import de.danoeh.antennapod.view.AspectRatioVideoView;
-/** Activity for playing audio files. */
-public class VideoplayerActivity extends MediaplayerActivity implements
- SurfaceHolder.Callback {
- private static final String TAG = "VideoplayerActivity";
+/**
+ * Activity for playing video files.
+ */
+public class VideoplayerActivity extends MediaplayerActivity {
+ private static final String TAG = "VideoplayerActivity";
- /** True if video controls are currently visible. */
- private boolean videoControlsShowing = true;
- private boolean videoSurfaceCreated = false;
- private VideoControlsHider videoControlsToggler;
+ /**
+ * True if video controls are currently visible.
+ */
+ private boolean videoControlsShowing = true;
+ private boolean videoSurfaceCreated = false;
+ private VideoControlsHider videoControlsToggler;
- private LinearLayout videoOverlay;
- private VideoView videoview;
- private ProgressBar progressIndicator;
+ private LinearLayout videoOverlay;
+ private AspectRatioVideoView videoview;
+ private ProgressBar progressIndicator;
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- requestWindowFeature(Window.FEATURE_ACTION_BAR_OVERLAY);
- setTheme(UserPreferences.getTheme());
+ @Override
+ protected void chooseTheme() {
+ setTheme(R.style.Theme_AntennaPod_Dark);
+ }
- super.onCreate(savedInstanceState);
- }
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ if (Build.VERSION.SDK_INT >= 11) {
+ requestWindowFeature(Window.FEATURE_ACTION_BAR_OVERLAY);
+ }
+ super.onCreate(savedInstanceState);
+ getSupportActionBar().setBackgroundDrawable(new ColorDrawable(0x80000000));
+ }
- @Override
- protected void onPause() {
- super.onPause();
- if (videoControlsToggler != null) {
- videoControlsToggler.cancel(true);
- }
- }
+ @Override
+ protected void onPause() {
+ super.onPause();
+ if (videoControlsToggler != null) {
+ videoControlsToggler.cancel(true);
+ }
+ if (controller != null && controller.getStatus() == PlayerStatus.PLAYING) {
+ controller.pause();
+ }
+ }
- @Override
- protected void onResume() {
- super.onResume();
- if (getIntent().getAction() != null
- && getIntent().getAction().equals(Intent.ACTION_VIEW)) {
- Intent intent = getIntent();
- if (AppConfig.DEBUG)
- Log.d(TAG, "Received VIEW intent: "
- + intent.getData().getPath());
- ExternalMedia media = new ExternalMedia(intent.getData().getPath(),
- MediaType.VIDEO);
- Intent launchIntent = new Intent(this, PlaybackService.class);
- launchIntent.putExtra(PlaybackService.EXTRA_PLAYABLE, media);
- launchIntent.putExtra(PlaybackService.EXTRA_START_WHEN_PREPARED,
- true);
- launchIntent.putExtra(PlaybackService.EXTRA_SHOULD_STREAM, false);
- launchIntent.putExtra(PlaybackService.EXTRA_PREPARE_IMMEDIATELY,
- true);
- startService(launchIntent);
- }
- }
+ @Override
+ protected void onResume() {
+ super.onResume();
+ if (getIntent().getAction() != null
+ && getIntent().getAction().equals(Intent.ACTION_VIEW)) {
+ Intent intent = getIntent();
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Received VIEW intent: "
+ + intent.getData().getPath());
+ ExternalMedia media = new ExternalMedia(intent.getData().getPath(),
+ MediaType.VIDEO);
+ Intent launchIntent = new Intent(this, PlaybackService.class);
+ launchIntent.putExtra(PlaybackService.EXTRA_PLAYABLE, media);
+ launchIntent.putExtra(PlaybackService.EXTRA_START_WHEN_PREPARED,
+ true);
+ launchIntent.putExtra(PlaybackService.EXTRA_SHOULD_STREAM, false);
+ launchIntent.putExtra(PlaybackService.EXTRA_PREPARE_IMMEDIATELY,
+ true);
+ startService(launchIntent);
+ }
+ }
- @Override
- protected void loadMediaInfo() {
- super.loadMediaInfo();
- Playable media = controller.getMedia();
- if (media != null) {
- getSupportActionBar().setSubtitle(media.getEpisodeTitle());
- getSupportActionBar().setTitle(media.getFeedTitle());
- }
- }
+ @Override
+ protected boolean loadMediaInfo() {
+ if (!super.loadMediaInfo()) {
+ return false;
+ }
+ Playable media = controller.getMedia();
+ if (media != null) {
+ getSupportActionBar().setSubtitle(media.getEpisodeTitle());
+ getSupportActionBar().setTitle(media.getFeedTitle());
+ return true;
+ }
- @Override
- protected void setupGUI() {
- super.setupGUI();
- videoOverlay = (LinearLayout) findViewById(R.id.overlay);
- videoview = (VideoView) findViewById(R.id.videoview);
- progressIndicator = (ProgressBar) findViewById(R.id.progressIndicator);
- videoview.getHolder().addCallback(this);
- videoview.setOnTouchListener(onVideoviewTouched);
+ return false;
+ }
- setupVideoControlsToggler();
- getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
- WindowManager.LayoutParams.FLAG_FULLSCREEN);
- }
+ @Override
+ protected void setupGUI() {
+ super.setupGUI();
+ videoOverlay = (LinearLayout) findViewById(R.id.overlay);
+ videoview = (AspectRatioVideoView) findViewById(R.id.videoview);
+ progressIndicator = (ProgressBar) findViewById(R.id.progressIndicator);
+ videoview.getHolder().addCallback(surfaceHolderCallback);
+ videoview.setOnTouchListener(onVideoviewTouched);
- @Override
- protected void onAwaitingVideoSurface() {
- if (videoSurfaceCreated) {
- if (AppConfig.DEBUG)
- Log.d(TAG,
- "Videosurface already created, setting videosurface now");
- controller.setVideoSurface(videoview.getHolder());
- }
- }
+ setupVideoControlsToggler();
+ getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
+ WindowManager.LayoutParams.FLAG_FULLSCREEN);
+ }
- @Override
- protected void postStatusMsg(int resId) {
- if (resId == R.string.player_preparing_msg) {
- progressIndicator.setVisibility(View.VISIBLE);
- } else {
- progressIndicator.setVisibility(View.INVISIBLE);
- }
+ @Override
+ protected void onAwaitingVideoSurface() {
+ if (videoSurfaceCreated) {
+ if (AppConfig.DEBUG)
+ Log.d(TAG,
+ "Videosurface already created, setting videosurface now");
- }
+ Pair videoSize = controller.getVideoSize();
+ if (videoSize != null && videoSize.first > 0 && videoSize.second > 0) {
+ if (AppConfig.DEBUG) Log.d(TAG, "Width,height of video: " + videoSize.first + ", " + videoSize.second);
+ videoview.setVideoSize(videoSize.first, videoSize.second);
+ } else {
+ Log.e(TAG, "Could not determine video size");
+ }
+ controller.setVideoSurface(videoview.getHolder());
+ }
+ }
- @Override
- protected void clearStatusMsg() {
- progressIndicator.setVisibility(View.INVISIBLE);
- }
+ @Override
+ protected void postStatusMsg(int resId) {
+ if (resId == R.string.player_preparing_msg) {
+ progressIndicator.setVisibility(View.VISIBLE);
+ } else {
+ progressIndicator.setVisibility(View.INVISIBLE);
+ }
- View.OnTouchListener onVideoviewTouched = new View.OnTouchListener() {
+ }
- @Override
- public boolean onTouch(View v, MotionEvent event) {
- if (event.getAction() == MotionEvent.ACTION_DOWN) {
- if (videoControlsToggler != null) {
- videoControlsToggler.cancel(true);
- }
- toggleVideoControlsVisibility();
- if (videoControlsShowing) {
- setupVideoControlsToggler();
- }
+ @Override
+ protected void clearStatusMsg() {
+ progressIndicator.setVisibility(View.INVISIBLE);
+ }
- return true;
- } else {
- return false;
- }
- }
- };
+ View.OnTouchListener onVideoviewTouched = new View.OnTouchListener() {
- @SuppressLint("NewApi")
- void setupVideoControlsToggler() {
- if (videoControlsToggler != null) {
- videoControlsToggler.cancel(true);
- }
- videoControlsToggler = new VideoControlsHider();
- if (android.os.Build.VERSION.SDK_INT > android.os.Build.VERSION_CODES.GINGERBREAD_MR1) {
- videoControlsToggler
- .executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
- } else {
- videoControlsToggler.execute();
- }
- }
+ @Override
+ public boolean onTouch(View v, MotionEvent event) {
+ if (event.getAction() == MotionEvent.ACTION_DOWN) {
+ if (videoControlsToggler != null) {
+ videoControlsToggler.cancel(true);
+ }
+ toggleVideoControlsVisibility();
+ if (videoControlsShowing) {
+ setupVideoControlsToggler();
+ }
- private void toggleVideoControlsVisibility() {
- if (videoControlsShowing) {
- getSupportActionBar().hide();
- hideVideoControls();
- } else {
- getSupportActionBar().show();
- showVideoControls();
- }
- videoControlsShowing = !videoControlsShowing;
- }
+ return true;
+ } else {
+ return false;
+ }
+ }
+ };
- /** Hides the videocontrols after a certain period of time. */
- public class VideoControlsHider extends AsyncTask {
- @Override
- protected void onCancelled() {
- videoControlsToggler = null;
- }
+ @SuppressLint("NewApi")
+ void setupVideoControlsToggler() {
+ if (videoControlsToggler != null) {
+ videoControlsToggler.cancel(true);
+ }
+ videoControlsToggler = new VideoControlsHider();
+ if (android.os.Build.VERSION.SDK_INT > android.os.Build.VERSION_CODES.GINGERBREAD_MR1) {
+ videoControlsToggler
+ .executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ } else {
+ videoControlsToggler.execute();
+ }
+ }
- @Override
- protected void onPostExecute(Void result) {
- videoControlsToggler = null;
- }
+ private void toggleVideoControlsVisibility() {
+ if (videoControlsShowing) {
+ getSupportActionBar().hide();
+ hideVideoControls();
+ } else {
+ getSupportActionBar().show();
+ showVideoControls();
+ }
+ videoControlsShowing = !videoControlsShowing;
+ }
- private static final int WAITING_INTERVALL = 5000;
- private static final String TAG = "VideoControlsToggler";
+ /**
+ * Hides the videocontrols after a certain period of time.
+ */
+ public class VideoControlsHider extends AsyncTask {
+ @Override
+ protected void onCancelled() {
+ videoControlsToggler = null;
+ }
- @Override
- protected void onProgressUpdate(Void... values) {
- if (videoControlsShowing) {
- if (AppConfig.DEBUG)
- Log.d(TAG, "Hiding video controls");
- getSupportActionBar().hide();
- hideVideoControls();
- videoControlsShowing = false;
- }
- }
+ @Override
+ protected void onPostExecute(Void result) {
+ videoControlsToggler = null;
+ }
- @Override
- protected Void doInBackground(Void... params) {
- try {
- Thread.sleep(WAITING_INTERVALL);
- } catch (InterruptedException e) {
- return null;
- }
- publishProgress();
- return null;
- }
+ private static final int WAITING_INTERVALL = 5000;
+ private static final String TAG = "VideoControlsToggler";
- }
+ @Override
+ protected void onProgressUpdate(Void... values) {
+ if (videoControlsShowing) {
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Hiding video controls");
+ getSupportActionBar().hide();
+ hideVideoControls();
+ videoControlsShowing = false;
+ }
+ }
- @Override
- public void surfaceChanged(SurfaceHolder holder, int format, int width,
- int height) {
- holder.setFixedSize(width, height);
- }
+ @Override
+ protected Void doInBackground(Void... params) {
+ try {
+ Thread.sleep(WAITING_INTERVALL);
+ } catch (InterruptedException e) {
+ return null;
+ }
+ publishProgress();
+ return null;
+ }
- @Override
- public void surfaceCreated(SurfaceHolder holder) {
- if (AppConfig.DEBUG)
- Log.d(TAG, "Videoview holder created");
- videoSurfaceCreated = true;
- if (controller.getStatus() == PlayerStatus.AWAITING_VIDEO_SURFACE) {
- if (controller.serviceAvailable()) {
- controller.setVideoSurface(holder);
- } else {
- Log.e(TAG,
- "Could'nt attach surface to mediaplayer - reference to service was null");
- }
- }
+ }
- }
+ private final SurfaceHolder.Callback surfaceHolderCallback = new SurfaceHolder.Callback() {
+ @Override
+ public void surfaceChanged(SurfaceHolder holder, int format, int width,
+ int height) {
+ holder.setFixedSize(width, height);
+ }
- @Override
- public void surfaceDestroyed(SurfaceHolder holder) {
- if (AppConfig.DEBUG)
- Log.d(TAG, "Videosurface was destroyed");
- videoSurfaceCreated = false;
- controller.notifyVideoSurfaceAbandoned();
- }
+ @Override
+ public void surfaceCreated(SurfaceHolder holder) {
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Videoview holder created");
+ videoSurfaceCreated = true;
+ if (controller.getStatus() == PlayerStatus.PLAYING) {
+ if (controller.serviceAvailable()) {
+ controller.setVideoSurface(holder);
+ } else {
+ Log.e(TAG,
+ "Could'nt attach surface to mediaplayer - reference to service was null");
+ }
+ }
- @Override
- protected void onReloadNotification(int notificationCode) {
- if (notificationCode == PlaybackService.EXTRA_CODE_AUDIO) {
- if (AppConfig.DEBUG)
- Log.d(TAG,
- "ReloadNotification received, switching to Audioplayer now");
- startActivity(new Intent(this, AudioplayerActivity.class));
- }
- }
+ }
- @Override
- public void onStartTrackingTouch(SeekBar seekBar) {
- super.onStartTrackingTouch(seekBar);
- if (videoControlsToggler != null) {
- videoControlsToggler.cancel(true);
- }
- }
+ @Override
+ public void surfaceDestroyed(SurfaceHolder holder) {
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Videosurface was destroyed");
+ videoSurfaceCreated = false;
+ controller.notifyVideoSurfaceAbandoned();
+ }
+ };
- @Override
- public void onStopTrackingTouch(SeekBar seekBar) {
- super.onStopTrackingTouch(seekBar);
- setupVideoControlsToggler();
- }
- @Override
- protected void onBufferStart() {
- progressIndicator.setVisibility(View.VISIBLE);
- }
+ @Override
+ protected void onReloadNotification(int notificationCode) {
+ if (notificationCode == PlaybackService.EXTRA_CODE_AUDIO) {
+ if (AppConfig.DEBUG)
+ Log.d(TAG,
+ "ReloadNotification received, switching to Audioplayer now");
+ finish();
+ startActivity(new Intent(this, AudioplayerActivity.class));
+ }
+ }
- @Override
- protected void onBufferEnd() {
- progressIndicator.setVisibility(View.INVISIBLE);
- }
+ @Override
+ public void onStartTrackingTouch(SeekBar seekBar) {
+ super.onStartTrackingTouch(seekBar);
+ if (videoControlsToggler != null) {
+ videoControlsToggler.cancel(true);
+ }
+ }
- private void showVideoControls() {
- videoOverlay.setVisibility(View.VISIBLE);
- videoOverlay.startAnimation(AnimationUtils.loadAnimation(this,
- R.anim.fade_in));
- }
+ @Override
+ public void onStopTrackingTouch(SeekBar seekBar) {
+ super.onStopTrackingTouch(seekBar);
+ setupVideoControlsToggler();
+ }
- private void hideVideoControls() {
- videoOverlay.startAnimation(AnimationUtils.loadAnimation(this,
- R.anim.fade_out));
- videoOverlay.setVisibility(View.GONE);
- }
+ @Override
+ protected void onBufferStart() {
+ progressIndicator.setVisibility(View.VISIBLE);
+ }
- @Override
- protected int getContentViewResourceId() {
- return R.layout.videoplayer_activity;
- }
+ @Override
+ protected void onBufferEnd() {
+ progressIndicator.setVisibility(View.INVISIBLE);
+ }
+
+ private void showVideoControls() {
+ videoOverlay.setVisibility(View.VISIBLE);
+ butPlay.setVisibility(View.VISIBLE);
+ final Animation animation = AnimationUtils.loadAnimation(this,
+ R.anim.fade_in);
+ if (animation != null) {
+ videoOverlay.startAnimation(animation);
+ butPlay.startAnimation(animation);
+ }
+ if (Build.VERSION.SDK_INT >= 14) {
+ videoview.setSystemUiVisibility(View.SYSTEM_UI_FLAG_VISIBLE);
+ }
+ }
+
+ private void hideVideoControls() {
+ final Animation animation = AnimationUtils.loadAnimation(this,
+ R.anim.fade_out);
+ if (animation != null) {
+ videoOverlay.startAnimation(animation);
+ butPlay.startAnimation(animation);
+ }
+ if (Build.VERSION.SDK_INT >= 14) {
+ videoview.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LOW_PROFILE);
+ }
+ videoOverlay.setVisibility(View.GONE);
+ butPlay.setVisibility(View.GONE);
+ }
+
+ @Override
+ protected int getContentViewResourceId() {
+ return R.layout.videoplayer_activity;
+ }
}
diff --git a/src/de/danoeh/antennapod/fragment/ExternalPlayerFragment.java b/src/de/danoeh/antennapod/fragment/ExternalPlayerFragment.java
index 3f967bbbe..56e6ee4b8 100644
--- a/src/de/danoeh/antennapod/fragment/ExternalPlayerFragment.java
+++ b/src/de/danoeh/antennapod/fragment/ExternalPlayerFragment.java
@@ -14,7 +14,7 @@ import android.widget.TextView;
import de.danoeh.antennapod.AppConfig;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.asynctask.ImageLoader;
-import de.danoeh.antennapod.service.PlaybackService;
+import de.danoeh.antennapod.service.playback.PlaybackService;
import de.danoeh.antennapod.util.Converter;
import de.danoeh.antennapod.util.playback.Playable;
import de.danoeh.antennapod.util.playback.PlaybackController;
@@ -137,10 +137,12 @@ public class ExternalPlayerFragment extends Fragment {
}
@Override
- public void loadMediaInfo() {
+ public boolean loadMediaInfo() {
ExternalPlayerFragment fragment = ExternalPlayerFragment.this;
if (fragment != null) {
- fragment.loadMediaInfo();
+ return fragment.loadMediaInfo();
+ } else {
+ return false;
}
}
@@ -209,7 +211,7 @@ public class ExternalPlayerFragment extends Fragment {
}
}
- private void loadMediaInfo() {
+ private boolean loadMediaInfo() {
if (AppConfig.DEBUG)
Log.d(TAG, "Loading media info");
if (controller.serviceAvailable()) {
@@ -230,13 +232,16 @@ public class ExternalPlayerFragment extends Fragment {
} else {
butPlay.setVisibility(View.VISIBLE);
}
+ return true;
} else {
Log.w(TAG,
"loadMediaInfo was called while the media object of playbackService was null!");
+ return false;
}
} else {
Log.w(TAG,
"loadMediaInfo was called while playbackService was null!");
+ return false;
}
}
diff --git a/src/de/danoeh/antennapod/preferences/UserPreferences.java b/src/de/danoeh/antennapod/preferences/UserPreferences.java
index f00d6245c..69714c6a8 100644
--- a/src/de/danoeh/antennapod/preferences/UserPreferences.java
+++ b/src/de/danoeh/antennapod/preferences/UserPreferences.java
@@ -319,7 +319,9 @@ public class UserPreferences implements
PREF_PLAYBACK_SPEED_ARRAY, null));
} else if (key.equals(PREF_PAUSE_PLAYBACK_FOR_FOCUS_LOSS)) {
pauseForFocusLoss = sp.getBoolean(PREF_PAUSE_PLAYBACK_FOR_FOCUS_LOSS, false);
- }
+ } else if (key.equals(PREF_PAUSE_ON_HEADSET_DISCONNECT)) {
+ pauseOnHeadsetDisconnect = sp.getBoolean(PREF_PAUSE_ON_HEADSET_DISCONNECT, true);
+ }
}
public static void setPlaybackSpeed(String speed) {
diff --git a/src/de/danoeh/antennapod/receiver/MediaButtonReceiver.java b/src/de/danoeh/antennapod/receiver/MediaButtonReceiver.java
index c57070091..a53ad486a 100644
--- a/src/de/danoeh/antennapod/receiver/MediaButtonReceiver.java
+++ b/src/de/danoeh/antennapod/receiver/MediaButtonReceiver.java
@@ -6,7 +6,7 @@ import android.content.Intent;
import android.util.Log;
import android.view.KeyEvent;
import de.danoeh.antennapod.AppConfig;
-import de.danoeh.antennapod.service.PlaybackService;
+import de.danoeh.antennapod.service.playback.PlaybackService;
/** Receives media button events. */
public class MediaButtonReceiver extends BroadcastReceiver {
diff --git a/src/de/danoeh/antennapod/receiver/PlayerWidget.java b/src/de/danoeh/antennapod/receiver/PlayerWidget.java
index a3d849972..25bb53475 100644
--- a/src/de/danoeh/antennapod/receiver/PlayerWidget.java
+++ b/src/de/danoeh/antennapod/receiver/PlayerWidget.java
@@ -6,7 +6,7 @@ import android.content.Context;
import android.content.Intent;
import android.util.Log;
import de.danoeh.antennapod.AppConfig;
-import de.danoeh.antennapod.service.PlayerWidgetService;
+import de.danoeh.antennapod.service.playback.PlayerWidgetService;
public class PlayerWidget extends AppWidgetProvider {
private static final String TAG = "PlayerWidget";
diff --git a/src/de/danoeh/antennapod/service/PlaybackService.java b/src/de/danoeh/antennapod/service/PlaybackService.java
deleted file mode 100644
index 2fa4e10d9..000000000
--- a/src/de/danoeh/antennapod/service/PlaybackService.java
+++ /dev/null
@@ -1,1736 +0,0 @@
-package de.danoeh.antennapod.service;
-
-import java.io.IOException;
-import java.util.Date;
-import java.util.List;
-import java.util.concurrent.*;
-
-import android.annotation.SuppressLint;
-import android.app.Notification;
-import android.app.PendingIntent;
-import android.app.Service;
-import android.content.BroadcastReceiver;
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.content.SharedPreferences;
-import android.graphics.Bitmap;
-import android.graphics.BitmapFactory;
-import android.media.AudioManager;
-import android.media.AudioManager.OnAudioFocusChangeListener;
-import android.media.MediaMetadataRetriever;
-import android.media.MediaPlayer;
-import android.media.RemoteControlClient;
-import android.media.RemoteControlClient.MetadataEditor;
-import android.os.AsyncTask;
-import android.os.Binder;
-import android.os.IBinder;
-import android.preference.PreferenceManager;
-import android.support.v4.app.NotificationCompat;
-import android.util.Log;
-import android.view.KeyEvent;
-import android.view.SurfaceHolder;
-import de.danoeh.antennapod.AppConfig;
-import de.danoeh.antennapod.R;
-import de.danoeh.antennapod.activity.AudioplayerActivity;
-import de.danoeh.antennapod.activity.VideoplayerActivity;
-import de.danoeh.antennapod.feed.*;
-import de.danoeh.antennapod.preferences.PlaybackPreferences;
-import de.danoeh.antennapod.preferences.UserPreferences;
-import de.danoeh.antennapod.receiver.MediaButtonReceiver;
-import de.danoeh.antennapod.receiver.PlayerWidget;
-import de.danoeh.antennapod.storage.DBReader;
-import de.danoeh.antennapod.storage.DBTasks;
-import de.danoeh.antennapod.storage.DBWriter;
-import de.danoeh.antennapod.util.BitmapDecoder;
-import de.danoeh.antennapod.util.QueueAccess;
-import de.danoeh.antennapod.util.DuckType;
-import de.danoeh.antennapod.util.flattr.FlattrUtils;
-import de.danoeh.antennapod.util.playback.AudioPlayer;
-import de.danoeh.antennapod.util.playback.IPlayer;
-import de.danoeh.antennapod.util.playback.Playable;
-import de.danoeh.antennapod.util.playback.Playable.PlayableException;
-import de.danoeh.antennapod.util.playback.VideoPlayer;
-import de.danoeh.antennapod.util.playback.PlaybackController;
-
-/**
- * Controls the MediaPlayer that plays a FeedMedia-file
- */
-public class PlaybackService extends Service {
- /**
- * Logging tag
- */
- private static final String TAG = "PlaybackService";
-
- /**
- * Parcelable of type Playable.
- */
- public static final String EXTRA_PLAYABLE = "PlaybackService.PlayableExtra";
- /**
- * True if media should be streamed.
- */
- public static final String EXTRA_SHOULD_STREAM = "extra.de.danoeh.antennapod.service.shouldStream";
- /**
- * True if playback should be started immediately after media has been
- * prepared.
- */
- public static final String EXTRA_START_WHEN_PREPARED = "extra.de.danoeh.antennapod.service.startWhenPrepared";
-
- public static final String EXTRA_PREPARE_IMMEDIATELY = "extra.de.danoeh.antennapod.service.prepareImmediately";
-
- public static final String ACTION_PLAYER_STATUS_CHANGED = "action.de.danoeh.antennapod.service.playerStatusChanged";
- private static final String AVRCP_ACTION_PLAYER_STATUS_CHANGED = "com.android.music.playstatechanged";
-
- public static final String ACTION_PLAYER_NOTIFICATION = "action.de.danoeh.antennapod.service.playerNotification";
- public static final String EXTRA_NOTIFICATION_CODE = "extra.de.danoeh.antennapod.service.notificationCode";
- public static final String EXTRA_NOTIFICATION_TYPE = "extra.de.danoeh.antennapod.service.notificationType";
-
- /**
- * If the PlaybackService receives this action, it will stop playback and
- * try to shutdown.
- */
- public static final String ACTION_SHUTDOWN_PLAYBACK_SERVICE = "action.de.danoeh.antennapod.service.actionShutdownPlaybackService";
-
- /**
- * If the PlaybackService receives this action, it will end playback of the
- * current episode and load the next episode if there is one available.
- */
- public static final String ACTION_SKIP_CURRENT_EPISODE = "action.de.danoeh.antennapod.service.skipCurrentEpisode";
-
- /**
- * Used in NOTIFICATION_TYPE_RELOAD.
- */
- public static final int EXTRA_CODE_AUDIO = 1;
- public static final int EXTRA_CODE_VIDEO = 2;
-
- public static final int NOTIFICATION_TYPE_ERROR = 0;
- public static final int NOTIFICATION_TYPE_INFO = 1;
- public static final int NOTIFICATION_TYPE_BUFFER_UPDATE = 2;
-
- /**
- * Receivers of this intent should update their information about the curently playing media
- */
- public static final int NOTIFICATION_TYPE_RELOAD = 3;
- /**
- * The state of the sleeptimer changed.
- */
- public static final int NOTIFICATION_TYPE_SLEEPTIMER_UPDATE = 4;
- public static final int NOTIFICATION_TYPE_BUFFER_START = 5;
- public static final int NOTIFICATION_TYPE_BUFFER_END = 6;
- /**
- * No more episodes are going to be played.
- */
- public static final int NOTIFICATION_TYPE_PLAYBACK_END = 7;
-
- /**
- * Playback speed has changed
- * */
- public static final int NOTIFICATION_TYPE_PLAYBACK_SPEED_CHANGE = 8;
-
- /**
- * Returned by getPositionSafe() or getDurationSafe() if the playbackService
- * is in an invalid state.
- */
- public static final int INVALID_TIME = -1;
-
- /**
- * Is true if service is running.
- */
- public static boolean isRunning = false;
-
- private static final int NOTIFICATION_ID = 1;
-
- private volatile IPlayer player;
- private RemoteControlClient remoteControlClient;
- private AudioManager audioManager;
- private ComponentName mediaButtonReceiver;
-
- private volatile Playable media;
-
- /**
- * True if media should be streamed (Extracted from Intent Extra) .
- */
- private boolean shouldStream;
-
- private boolean startWhenPrepared;
- private PlayerStatus status;
-
- private PositionSaver positionSaver;
- private ScheduledFuture positionSaverFuture;
-
- private WidgetUpdateWorker widgetUpdater;
- private ScheduledFuture widgetUpdaterFuture;
-
- private SleepTimer sleepTimer;
- private Future sleepTimerFuture;
-
- private static final int SCHED_EX_POOL_SIZE = 3;
- private ScheduledThreadPoolExecutor schedExecutor;
- private ExecutorService dbLoaderExecutor;
-
- private volatile PlayerStatus statusBeforeSeek;
-
- private static boolean playingVideo;
-
- /**
- * True if mediaplayer was paused because it lost audio focus temporarily
- */
- private boolean pausedBecauseOfTransientAudiofocusLoss;
-
- private Thread chapterLoader;
-
- private final IBinder mBinder = new LocalBinder();
-
- private volatile List queue;
-
- public class LocalBinder extends Binder {
- public PlaybackService getService() {
- return PlaybackService.this;
- }
- }
-
- @Override
- public boolean onUnbind(Intent intent) {
- if (AppConfig.DEBUG)
- Log.d(TAG, "Received onUnbind event");
- return super.onUnbind(intent);
- }
-
- /**
- * Returns an intent which starts an audio- or videoplayer, depending on the
- * type of media that is being played. If the playbackservice is not
- * running, the type of the last played media will be looked up.
- */
- public static Intent getPlayerActivityIntent(Context context) {
- if (isRunning) {
- if (playingVideo) {
- return new Intent(context, VideoplayerActivity.class);
- } else {
- return new Intent(context, AudioplayerActivity.class);
- }
- } else {
- if (PlaybackPreferences.getCurrentEpisodeIsVideo()) {
- return new Intent(context, VideoplayerActivity.class);
- } else {
- return new Intent(context, AudioplayerActivity.class);
- }
- }
- }
-
- /**
- * Same as getPlayerActivityIntent(context), but here the type of activity
- * depends on the FeedMedia that is provided as an argument.
- */
- public static Intent getPlayerActivityIntent(Context context, Playable media) {
- MediaType mt = media.getMediaType();
- if (mt == MediaType.VIDEO) {
- return new Intent(context, VideoplayerActivity.class);
- } else {
- return new Intent(context, AudioplayerActivity.class);
- }
- }
-
- @SuppressLint("NewApi")
- @Override
- public void onCreate() {
- super.onCreate();
- if (AppConfig.DEBUG)
- Log.d(TAG, "Service created.");
- isRunning = true;
- pausedBecauseOfTransientAudiofocusLoss = false;
- status = PlayerStatus.STOPPED;
- audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
- schedExecutor = new ScheduledThreadPoolExecutor(SCHED_EX_POOL_SIZE,
- new ThreadFactory() {
-
- @Override
- public Thread newThread(Runnable r) {
- Thread t = new Thread(r);
- t.setPriority(Thread.MIN_PRIORITY);
- return t;
- }
- }, new RejectedExecutionHandler() {
-
- @Override
- public void rejectedExecution(Runnable r,
- ThreadPoolExecutor executor) {
- Log.w(TAG, "SchedEx rejected submission of new task");
- }
- }
- );
- dbLoaderExecutor = Executors.newSingleThreadExecutor();
-
- mediaButtonReceiver = new ComponentName(getPackageName(),
- MediaButtonReceiver.class.getName());
- audioManager.registerMediaButtonEventReceiver(mediaButtonReceiver);
- if (android.os.Build.VERSION.SDK_INT >= 14) {
- audioManager
- .registerRemoteControlClient(setupRemoteControlClient());
- }
- registerReceiver(headsetDisconnected, new IntentFilter(
- Intent.ACTION_HEADSET_PLUG));
- registerReceiver(shutdownReceiver, new IntentFilter(
- ACTION_SHUTDOWN_PLAYBACK_SERVICE));
- registerReceiver(audioBecomingNoisy, new IntentFilter(
- AudioManager.ACTION_AUDIO_BECOMING_NOISY));
- registerReceiver(skipCurrentEpisodeReceiver, new IntentFilter(
- ACTION_SKIP_CURRENT_EPISODE));
- EventDistributor.getInstance().register(eventDistributorListener);
- loadQueue();
- }
-
- private IPlayer createMediaPlayer() {
- if (player != null) {
- player.release();
- }
- IPlayer player;
- if (media == null || media.getMediaType() == MediaType.VIDEO) {
- player = new VideoPlayer();
- } else {
- player = new AudioPlayer(this);
- }
- return createMediaPlayer(player);
- }
-
- private IPlayer createMediaPlayer(IPlayer mp) {
- if (mp != null && media != null) {
- if (media.getMediaType() == MediaType.AUDIO) {
- ((AudioPlayer) mp).setOnPreparedListener(audioPreparedListener);
- ((AudioPlayer) mp)
- .setOnCompletionListener(audioCompletionListener);
- ((AudioPlayer) mp)
- .setOnSeekCompleteListener(audioSeekCompleteListener);
- ((AudioPlayer) mp).setOnErrorListener(audioErrorListener);
- ((AudioPlayer) mp)
- .setOnBufferingUpdateListener(audioBufferingUpdateListener);
- ((AudioPlayer) mp).setOnInfoListener(audioInfoListener);
- } else {
- ((VideoPlayer) mp).setOnPreparedListener(videoPreparedListener);
- ((VideoPlayer) mp)
- .setOnCompletionListener(videoCompletionListener);
- ((VideoPlayer) mp)
- .setOnSeekCompleteListener(videoSeekCompleteListener);
- ((VideoPlayer) mp).setOnErrorListener(videoErrorListener);
- ((VideoPlayer) mp)
- .setOnBufferingUpdateListener(videoBufferingUpdateListener);
- ((VideoPlayer) mp).setOnInfoListener(videoInfoListener);
- }
- }
- return mp;
- }
-
- @SuppressLint("NewApi")
- @Override
- public void onDestroy() {
- super.onDestroy();
- if (AppConfig.DEBUG)
- Log.d(TAG, "Service is about to be destroyed");
- isRunning = false;
- if (chapterLoader != null) {
- chapterLoader.interrupt();
- }
- disableSleepTimer();
- unregisterReceiver(headsetDisconnected);
- unregisterReceiver(shutdownReceiver);
- unregisterReceiver(audioBecomingNoisy);
- unregisterReceiver(skipCurrentEpisodeReceiver);
- EventDistributor.getInstance().unregister(eventDistributorListener);
- if (android.os.Build.VERSION.SDK_INT >= 14) {
- audioManager.unregisterRemoteControlClient(remoteControlClient);
- }
- audioManager.unregisterMediaButtonEventReceiver(mediaButtonReceiver);
- audioManager.abandonAudioFocus(audioFocusChangeListener);
- player.release();
- stopWidgetUpdater();
- updateWidget();
- }
-
- @Override
- public IBinder onBind(Intent intent) {
- if (AppConfig.DEBUG)
- Log.d(TAG, "Received onBind event");
- return mBinder;
- }
-
- private final EventDistributor.EventListener eventDistributorListener = new EventDistributor.EventListener() {
- @Override
- public void update(EventDistributor eventDistributor, Integer arg) {
- if ((EventDistributor.QUEUE_UPDATE & arg) != 0) {
- loadQueue();
- }
- }
- };
-
- private final OnAudioFocusChangeListener audioFocusChangeListener = new OnAudioFocusChangeListener() {
-
- @Override
- public void onAudioFocusChange(int focusChange) {
- switch (focusChange) {
- case AudioManager.AUDIOFOCUS_LOSS:
- if (AppConfig.DEBUG)
- Log.d(TAG, "Lost audio focus");
- pause(true, false);
- stopSelf();
- break;
- case AudioManager.AUDIOFOCUS_GAIN:
- if (AppConfig.DEBUG)
- Log.d(TAG, "Gained audio focus");
- if (pausedBecauseOfTransientAudiofocusLoss) {
- audioManager.adjustStreamVolume(AudioManager.STREAM_MUSIC,
- AudioManager.ADJUST_RAISE, 0);
- play();
- }
- break;
- case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
- if (status == PlayerStatus.PLAYING) {
- if (!UserPreferences.shouldPauseForFocusLoss()) {
- if (AppConfig.DEBUG)
- Log.d(TAG, "Lost audio focus temporarily. Ducking...");
- audioManager.adjustStreamVolume(AudioManager.STREAM_MUSIC,
- AudioManager.ADJUST_LOWER, 0);
- pausedBecauseOfTransientAudiofocusLoss = true;
- } else {
- if (AppConfig.DEBUG)
- Log.d(TAG, "Lost audio focus temporarily. Could duck, but won't, pausing...");
- pause(false, false);
- pausedBecauseOfTransientAudiofocusLoss = true;
- }
- }
- break;
- case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
- if (status == PlayerStatus.PLAYING) {
- if (AppConfig.DEBUG)
- Log.d(TAG, "Lost audio focus temporarily. Pausing...");
- pause(false, false);
- pausedBecauseOfTransientAudiofocusLoss = true;
- }
- }
- }
- };
-
- /**
- * 1. Check type of intent
- * 1.1 Keycode -> handle keycode -> done
- * 1.2 Playable -> Step 2
- * 2. Handle playable
- * 2.1 Check current status
- * 2.1.1 Not playing -> play new playable
- * 2.1.2 Playing, new playable is the same -> play if playback is currently paused
- * 2.1.3 Playing, new playable different -> Stop playback of old media
- *
- * @param intent
- * @param flags
- * @param startId
- * @return
- */
- @Override
- public int onStartCommand(Intent intent, int flags, int startId) {
- super.onStartCommand(intent, flags, startId);
-
- if (AppConfig.DEBUG)
- Log.d(TAG, "OnStartCommand called");
- final int keycode = intent.getIntExtra(MediaButtonReceiver.EXTRA_KEYCODE, -1);
- final Playable playable = intent.getParcelableExtra(EXTRA_PLAYABLE);
- if (keycode == -1 && playable == null) {
- Log.e(TAG, "PlaybackService was started with no arguments");
- stopSelf();
- }
-
- if (keycode != -1) {
- if (AppConfig.DEBUG)
- Log.d(TAG, "Received media button event");
- handleKeycode(keycode);
- } else {
- boolean playbackType = intent.getBooleanExtra(EXTRA_SHOULD_STREAM,
- true);
- if (media == null) {
- media = playable;
- shouldStream = playbackType;
- startWhenPrepared = intent.getBooleanExtra(
- EXTRA_START_WHEN_PREPARED, false);
- initMediaplayer(intent.getBooleanExtra(EXTRA_PREPARE_IMMEDIATELY, false));
- sendNotificationBroadcast(NOTIFICATION_TYPE_RELOAD, 0);
- }
- if (media != null) {
- if (!playable.getIdentifier().equals(media.getIdentifier())) {
- // different media or different playback type
- pause(true, false);
- player.reset();
- media = playable;
- shouldStream = playbackType;
- startWhenPrepared = intent.getBooleanExtra(EXTRA_START_WHEN_PREPARED, false);
- initMediaplayer(intent.getBooleanExtra(EXTRA_PREPARE_IMMEDIATELY, false));
- sendNotificationBroadcast(NOTIFICATION_TYPE_RELOAD, 0);
- } else {
- // same media and same playback type
- if (status == PlayerStatus.PAUSED) {
- play();
- }
- }
- }
- }
-
- return Service.START_NOT_STICKY;
- }
-
- /** Handles media button events */
- private void handleKeycode(int keycode) {
- if (AppConfig.DEBUG)
- Log.d(TAG, "Handling keycode: " + keycode);
- switch (keycode) {
- case KeyEvent.KEYCODE_HEADSETHOOK:
- case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE:
- if (status == PlayerStatus.PLAYING) {
- pause(true, true);
- } else if (status == PlayerStatus.PAUSED) {
- play();
- } else if (status == PlayerStatus.PREPARING) {
- setStartWhenPrepared(!startWhenPrepared);
- } else if (status == PlayerStatus.INITIALIZED) {
- startWhenPrepared = true;
- prepare();
- }
- break;
- case KeyEvent.KEYCODE_MEDIA_PLAY:
- if (status == PlayerStatus.PAUSED) {
- play();
- } else if (status == PlayerStatus.INITIALIZED) {
- startWhenPrepared = true;
- prepare();
- }
- break;
- case KeyEvent.KEYCODE_MEDIA_PAUSE:
- if (status == PlayerStatus.PLAYING) {
- pause(true, true);
- }
- break;
- case KeyEvent.KEYCODE_MEDIA_FAST_FORWARD: {
- seekDelta(PlaybackController.DEFAULT_SEEK_DELTA);
- break;
- }
- case KeyEvent.KEYCODE_MEDIA_REWIND: {
- seekDelta(-PlaybackController.DEFAULT_SEEK_DELTA);
- break;
- }
- }
- }
-
- /**
- * Called by a mediaplayer Activity as soon as it has prepared its
- * mediaplayer.
- */
- public void setVideoSurface(SurfaceHolder sh) {
- if (AppConfig.DEBUG)
- Log.d(TAG, "Setting display");
- player.setDisplay(null);
- player.setDisplay(sh);
- if (status == PlayerStatus.STOPPED
- || status == PlayerStatus.AWAITING_VIDEO_SURFACE) {
- try {
- InitTask initTask = new InitTask() {
-
- @Override
- protected void onPostExecute(Playable result) {
- if (status == PlayerStatus.INITIALIZING) {
- if (result != null) {
- try {
- if (shouldStream) {
- player.setDataSource(media
- .getStreamUrl());
- setStatus(PlayerStatus.PREPARING);
- player.prepareAsync();
- } else {
- player.setDataSource(media
- .getLocalMediaUrl());
- setStatus(PlayerStatus.PREPARING);
- player.prepareAsync();
- }
- } catch (IOException e) {
- e.printStackTrace();
- }
- } else {
- setStatus(PlayerStatus.ERROR);
- sendBroadcast(new Intent(
- ACTION_SHUTDOWN_PLAYBACK_SERVICE));
- }
- }
- }
-
- @Override
- protected void onPreExecute() {
- setStatus(PlayerStatus.INITIALIZING);
- }
-
- };
- initTask.executeAsync(media);
- } catch (IllegalArgumentException e) {
- e.printStackTrace();
- } catch (SecurityException e) {
- e.printStackTrace();
- } catch (IllegalStateException e) {
- e.printStackTrace();
- }
- }
-
- }
-
- /**
- * Called when the surface holder of the mediaplayer has to be changed.
- */
- private void resetVideoSurface() {
- if (AppConfig.DEBUG)
- Log.d(TAG, "Resetting video surface");
- cancelPositionSaver();
- player.setDisplay(null);
- player.reset();
- player = createMediaPlayer();
- status = PlayerStatus.STOPPED;
- }
-
- public void notifyVideoSurfaceAbandoned() {
- resetVideoSurface();
- if (media != null) {
- initMediaplayer(true);
- }
- }
-
- /**
- * Called after service has extracted the media it is supposed to play.
- *
- * @param prepareImmediately True if service should prepare playback after it has been initialized
- */
- private void initMediaplayer(final boolean prepareImmediately) {
- if (AppConfig.DEBUG)
- Log.d(TAG, "Setting up media player");
- try {
- MediaType mediaType = media.getMediaType();
- player = createMediaPlayer();
- if (mediaType == MediaType.AUDIO) {
- if (AppConfig.DEBUG)
- Log.d(TAG, "Mime type is audio");
-
- InitTask initTask = new InitTask() {
-
- @Override
- protected void onPostExecute(Playable result) {
- // check if state of service has changed. If it has
- // changed, assume that loaded metadata is not needed
- // anymore.
- if (status == PlayerStatus.INITIALIZING) {
- if (result != null) {
- playingVideo = false;
- try {
- if (shouldStream) {
- player.setDataSource(media
- .getStreamUrl());
- } else if (media.localFileAvailable()) {
- player.setDataSource(media
- .getLocalMediaUrl());
- }
-
- if (prepareImmediately) {
- setStatus(PlayerStatus.PREPARING);
- player.prepareAsync();
- } else {
- setStatus(PlayerStatus.INITIALIZED);
- }
- } catch (IOException e) {
- e.printStackTrace();
- media = null;
- setStatus(PlayerStatus.ERROR);
- sendBroadcast(new Intent(
- ACTION_SHUTDOWN_PLAYBACK_SERVICE));
- }
- } else {
- Log.e(TAG, "InitTask could not load metadata");
- media = null;
- setStatus(PlayerStatus.ERROR);
- sendBroadcast(new Intent(
- ACTION_SHUTDOWN_PLAYBACK_SERVICE));
- }
- } else {
- if (AppConfig.DEBUG)
- Log.d(TAG,
- "Status of player has changed during initialization. Stopping init process.");
- }
- }
-
- @Override
- protected void onPreExecute() {
- setStatus(PlayerStatus.INITIALIZING);
- }
-
- };
- initTask.executeAsync(media);
- } else if (mediaType == MediaType.VIDEO) {
- if (AppConfig.DEBUG)
- Log.d(TAG, "Mime type is video");
- playingVideo = true;
- setStatus(PlayerStatus.AWAITING_VIDEO_SURFACE);
- player.setScreenOnWhilePlaying(true);
- }
-
- } catch (IllegalArgumentException e) {
- e.printStackTrace();
- } catch (SecurityException e) {
- e.printStackTrace();
- } catch (IllegalStateException e) {
- e.printStackTrace();
- }
- }
-
- private void setupPositionSaver() {
- if (positionSaverFuture == null
- || (positionSaverFuture.isCancelled() || positionSaverFuture
- .isDone())) {
-
- positionSaver = new PositionSaver();
- positionSaverFuture = schedExecutor.scheduleAtFixedRate(
- positionSaver, PositionSaver.WAITING_INTERVALL,
- PositionSaver.WAITING_INTERVALL, TimeUnit.MILLISECONDS);
- }
- }
-
- private void cancelPositionSaver() {
- if (positionSaverFuture != null) {
- boolean result = positionSaverFuture.cancel(true);
- if (AppConfig.DEBUG)
- Log.d(TAG, "PositionSaver cancelled. Result: " + result);
- }
- }
-
- private final com.aocate.media.MediaPlayer.OnPreparedListener audioPreparedListener = new com.aocate.media.MediaPlayer.OnPreparedListener() {
- @Override
- public void onPrepared(com.aocate.media.MediaPlayer mp) {
- genericOnPrepared(mp);
- }
- };
-
- private final android.media.MediaPlayer.OnPreparedListener videoPreparedListener = new android.media.MediaPlayer.OnPreparedListener() {
- @Override
- public void onPrepared(android.media.MediaPlayer mp) {
- genericOnPrepared(mp);
- }
- };
-
- private final void genericOnPrepared(Object inObj) {
- IPlayer mp = DuckType.coerce(inObj).to(IPlayer.class);
- if (AppConfig.DEBUG)
- Log.d(TAG, "Resource prepared");
- mp.seekTo(media.getPosition());
- if (media.getDuration() == 0) {
- if (AppConfig.DEBUG)
- Log.d(TAG, "Setting duration of media");
- media.setDuration(mp.getDuration());
- }
- setStatus(PlayerStatus.PREPARED);
- if (chapterLoader != null) {
- chapterLoader.interrupt();
- }
- chapterLoader = new Thread() {
- @Override
- public void run() {
- if (AppConfig.DEBUG)
- Log.d(TAG, "Chapter loader started");
- if (media != null && media.getChapters() == null) {
- media.loadChapterMarks();
- if (!isInterrupted() && media.getChapters() != null) {
- sendNotificationBroadcast(NOTIFICATION_TYPE_RELOAD,
- 0);
- }
- }
- if (AppConfig.DEBUG)
- Log.d(TAG, "Chapter loader stopped");
- }
- };
- chapterLoader.start();
-
- if (startWhenPrepared) {
- play();
- }
- }
-
- private final com.aocate.media.MediaPlayer.OnSeekCompleteListener audioSeekCompleteListener = new com.aocate.media.MediaPlayer.OnSeekCompleteListener() {
- @Override
- public void onSeekComplete(com.aocate.media.MediaPlayer mp) {
- genericSeekCompleteListener();
- }
- };
-
- private final android.media.MediaPlayer.OnSeekCompleteListener videoSeekCompleteListener = new android.media.MediaPlayer.OnSeekCompleteListener() {
- @Override
- public void onSeekComplete(android.media.MediaPlayer mp) {
- genericSeekCompleteListener();
- }
- };
-
- private final void genericSeekCompleteListener() {
- if (status == PlayerStatus.SEEKING) {
- setStatus(statusBeforeSeek);
- }
- }
-
- private final com.aocate.media.MediaPlayer.OnInfoListener audioInfoListener = new com.aocate.media.MediaPlayer.OnInfoListener() {
- @Override
- public boolean onInfo(com.aocate.media.MediaPlayer mp, int what,
- int extra) {
- return genericInfoListener(what);
- }
- };
-
- private final android.media.MediaPlayer.OnInfoListener videoInfoListener = new android.media.MediaPlayer.OnInfoListener() {
- @Override
- public boolean onInfo(android.media.MediaPlayer mp, int what, int extra) {
- return genericInfoListener(what);
- }
- };
-
- private boolean genericInfoListener(int what) {
- switch (what) {
- case MediaPlayer.MEDIA_INFO_BUFFERING_START:
- sendNotificationBroadcast(NOTIFICATION_TYPE_BUFFER_START, 0);
- return true;
- case MediaPlayer.MEDIA_INFO_BUFFERING_END:
- sendNotificationBroadcast(NOTIFICATION_TYPE_BUFFER_END, 0);
- return true;
- default:
- return false;
- }
- }
-
- private final com.aocate.media.MediaPlayer.OnErrorListener audioErrorListener = new com.aocate.media.MediaPlayer.OnErrorListener() {
- @Override
- public boolean onError(com.aocate.media.MediaPlayer mp, int what,
- int extra) {
- return genericOnError(mp, what, extra);
- }
- };
-
- private final android.media.MediaPlayer.OnErrorListener videoErrorListener = new android.media.MediaPlayer.OnErrorListener() {
- @Override
- public boolean onError(android.media.MediaPlayer mp, int what, int extra) {
- return genericOnError(mp, what, extra);
- }
- };
-
- private boolean genericOnError(Object inObj, int what, int extra) {
- final String TAG = "PlaybackService.onErrorListener";
- Log.w(TAG, "An error has occured: " + what + " " + extra);
- IPlayer mp = DuckType.coerce(inObj).to(IPlayer.class);
- if (mp.isPlaying()) {
- pause(true, true);
- }
- sendNotificationBroadcast(NOTIFICATION_TYPE_ERROR, what);
- setCurrentlyPlayingMedia(PlaybackPreferences.NO_MEDIA_PLAYING);
- stopSelf();
- return true;
- }
-
- private final com.aocate.media.MediaPlayer.OnCompletionListener audioCompletionListener = new com.aocate.media.MediaPlayer.OnCompletionListener() {
- @Override
- public void onCompletion(com.aocate.media.MediaPlayer mp) {
- genericOnCompletion();
- }
- };
-
- private final android.media.MediaPlayer.OnCompletionListener videoCompletionListener = new android.media.MediaPlayer.OnCompletionListener() {
- @Override
- public void onCompletion(android.media.MediaPlayer mp) {
- genericOnCompletion();
- }
- };
-
- private void genericOnCompletion() {
- endPlayback(true);
- }
-
- private final com.aocate.media.MediaPlayer.OnBufferingUpdateListener audioBufferingUpdateListener = new com.aocate.media.MediaPlayer.OnBufferingUpdateListener() {
- @Override
- public void onBufferingUpdate(com.aocate.media.MediaPlayer mp,
- int percent) {
- genericOnBufferingUpdate(percent);
- }
- };
-
- private final android.media.MediaPlayer.OnBufferingUpdateListener videoBufferingUpdateListener = new android.media.MediaPlayer.OnBufferingUpdateListener() {
- @Override
- public void onBufferingUpdate(android.media.MediaPlayer mp, int percent) {
- genericOnBufferingUpdate(percent);
- }
- };
-
- private void genericOnBufferingUpdate(int percent) {
- sendNotificationBroadcast(NOTIFICATION_TYPE_BUFFER_UPDATE, percent);
- }
-
- private void endPlayback(boolean playNextEpisode) {
- if (AppConfig.DEBUG)
- Log.d(TAG, "Playback ended");
- audioManager.abandonAudioFocus(audioFocusChangeListener);
-
- // Save state
- cancelPositionSaver();
-
- boolean isInQueue = false;
- FeedItem nextItem = null;
-
- if (media instanceof FeedMedia) {
- FeedItem item = ((FeedMedia) media).getItem();
- DBWriter.markItemRead(PlaybackService.this, item, true, true);
- nextItem = DBTasks.getQueueSuccessorOfItem(this, item.getId(), queue);
- isInQueue = media instanceof FeedMedia
- && QueueAccess.ItemListAccess(queue).contains(((FeedMedia) media).getItem().getId());
- if (isInQueue) {
- DBWriter.removeQueueItem(PlaybackService.this, item.getId(), true);
- }
- DBWriter.addItemToPlaybackHistory(PlaybackService.this, (FeedMedia) media);
- long autoDeleteMediaId = ((FeedComponent) media).getId();
- if (shouldStream) {
- autoDeleteMediaId = -1;
- }
- }
-
- // Load next episode if previous episode was in the queue and if there
- // is an episode in the queue left.
- // Start playback immediately if continuous playback is enabled
- boolean loadNextItem = isInQueue && nextItem != null;
- playNextEpisode = playNextEpisode && loadNextItem
- && UserPreferences.isFollowQueue();
- if (loadNextItem) {
- if (AppConfig.DEBUG)
- Log.d(TAG, "Loading next item in queue");
- media = nextItem.getMedia();
- }
- final boolean prepareImmediately;
- if (playNextEpisode) {
- if (AppConfig.DEBUG)
- Log.d(TAG, "Playback of next episode will start immediately.");
- prepareImmediately = startWhenPrepared = true;
- } else {
- if (AppConfig.DEBUG)
- Log.d(TAG, "No more episodes available to play");
- media = null;
- prepareImmediately = startWhenPrepared = false;
- stopForeground(true);
- stopWidgetUpdater();
- }
-
- int notificationCode = 0;
- if (media != null) {
- shouldStream = !media.localFileAvailable();
- if (media.getMediaType() == MediaType.AUDIO) {
- notificationCode = EXTRA_CODE_AUDIO;
- playingVideo = false;
- } else if (media.getMediaType() == MediaType.VIDEO) {
- notificationCode = EXTRA_CODE_VIDEO;
- }
- }
- writePlaybackPreferences();
- if (media != null) {
- resetVideoSurface();
- refreshRemoteControlClientState();
- initMediaplayer(prepareImmediately);
-
- sendNotificationBroadcast(NOTIFICATION_TYPE_RELOAD,
- notificationCode);
- } else {
- sendNotificationBroadcast(NOTIFICATION_TYPE_PLAYBACK_END, 0);
- stopSelf();
- }
- }
-
- public void setSleepTimer(long waitingTime) {
- if (AppConfig.DEBUG)
- Log.d(TAG, "Setting sleep timer to " + Long.toString(waitingTime)
- + " milliseconds");
- if (sleepTimerFuture != null) {
- sleepTimerFuture.cancel(true);
- }
- sleepTimer = new SleepTimer(waitingTime);
- sleepTimerFuture = schedExecutor.submit(sleepTimer);
- sendNotificationBroadcast(NOTIFICATION_TYPE_SLEEPTIMER_UPDATE, 0);
- }
-
- public void disableSleepTimer() {
- if (sleepTimerFuture != null) {
- if (AppConfig.DEBUG)
- Log.d(TAG, "Disabling sleep timer");
- sleepTimerFuture.cancel(true);
- sendNotificationBroadcast(NOTIFICATION_TYPE_SLEEPTIMER_UPDATE, 0);
- }
- }
-
- /**
- * Saves the current position and pauses playback. Note that, if audiofocus
- * is abandoned, the lockscreen controls will also disapear.
- *
- * @param abandonFocus
- * is true if the service should release audio focus
- * @param reinit
- * is true if service should reinit after pausing if the media
- * file is being streamed
- */
- public void pause(boolean abandonFocus, boolean reinit) {
- if (player.isPlaying()) {
- if (AppConfig.DEBUG)
- Log.d(TAG, "Pausing playback.");
- player.pause();
- cancelPositionSaver();
- saveCurrentPosition();
- setStatus(PlayerStatus.PAUSED);
- if (abandonFocus) {
- audioManager.abandonAudioFocus(audioFocusChangeListener);
- pausedBecauseOfTransientAudiofocusLoss = false;
- disableSleepTimer();
- }
- stopWidgetUpdater();
- stopForeground(true);
- if (shouldStream && reinit) {
- reinit();
- }
- }
- }
-
- /** Pauses playback and destroys service. Recommended for video playback. */
- public void stop() {
- if (AppConfig.DEBUG)
- Log.d(TAG, "Stopping playback");
- if (status == PlayerStatus.PREPARED || status == PlayerStatus.PAUSED
- || status == PlayerStatus.STOPPED
- || status == PlayerStatus.PLAYING) {
- player.stop();
- }
- setCurrentlyPlayingMedia(PlaybackPreferences.NO_MEDIA_PLAYING);
- stopSelf();
- }
-
- /**
- * Prepared media player for playback if the service is in the INITALIZED
- * state.
- */
- public void prepare() {
- if (status == PlayerStatus.INITIALIZED) {
- if (AppConfig.DEBUG)
- Log.d(TAG, "Preparing media player");
- setStatus(PlayerStatus.PREPARING);
- player.prepareAsync();
- }
- }
-
- /** Resets the media player and moves into INITIALIZED state. */
- public void reinit() {
- player.reset();
- player = createMediaPlayer(player);
- initMediaplayer(false);
- }
-
- @SuppressLint("NewApi")
- public void play() {
- if (status == PlayerStatus.PAUSED || status == PlayerStatus.PREPARED
- || status == PlayerStatus.STOPPED) {
- int focusGained = audioManager.requestAudioFocus(
- audioFocusChangeListener, AudioManager.STREAM_MUSIC,
- AudioManager.AUDIOFOCUS_GAIN);
-
- if (focusGained == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
- if (AppConfig.DEBUG)
- Log.d(TAG, "Audiofocus successfully requested");
- if (AppConfig.DEBUG)
- Log.d(TAG, "Resuming/Starting playback");
- writePlaybackPreferences();
-
- setSpeed(Float.parseFloat(UserPreferences.getPlaybackSpeed()));
- player.start();
- if (status != PlayerStatus.PAUSED) {
- player.seekTo((int) media.getPosition());
- }
- setStatus(PlayerStatus.PLAYING);
- setupPositionSaver();
- setupWidgetUpdater();
- setupNotification();
- pausedBecauseOfTransientAudiofocusLoss = false;
- if (android.os.Build.VERSION.SDK_INT >= 14) {
- audioManager
- .registerRemoteControlClient(remoteControlClient);
- }
- audioManager
- .registerMediaButtonEventReceiver(mediaButtonReceiver);
- media.onPlaybackStart();
- } else {
- if (AppConfig.DEBUG)
- Log.d(TAG, "Failed to request Audiofocus");
- }
- }
- }
-
- private void writePlaybackPreferences() {
- if (AppConfig.DEBUG)
- Log.d(TAG, "Writing playback preferences");
-
- SharedPreferences.Editor editor = PreferenceManager
- .getDefaultSharedPreferences(getApplicationContext()).edit();
- if (media != null) {
- editor.putLong(PlaybackPreferences.PREF_CURRENTLY_PLAYING_MEDIA,
- media.getPlayableType());
- editor.putBoolean(
- PlaybackPreferences.PREF_CURRENT_EPISODE_IS_STREAM,
- shouldStream);
- editor.putBoolean(
- PlaybackPreferences.PREF_CURRENT_EPISODE_IS_VIDEO,
- playingVideo);
- if (media instanceof FeedMedia) {
- FeedMedia fMedia = (FeedMedia) media;
- editor.putLong(
- PlaybackPreferences.PREF_CURRENTLY_PLAYING_FEED_ID,
- fMedia.getItem().getFeed().getId());
- editor.putLong(
- PlaybackPreferences.PREF_CURRENTLY_PLAYING_FEEDMEDIA_ID,
- fMedia.getId());
- } else {
- editor.putLong(
- PlaybackPreferences.PREF_CURRENTLY_PLAYING_FEED_ID,
- PlaybackPreferences.NO_MEDIA_PLAYING);
- editor.putLong(
- PlaybackPreferences.PREF_CURRENTLY_PLAYING_FEEDMEDIA_ID,
- PlaybackPreferences.NO_MEDIA_PLAYING);
- }
- media.writeToPreferences(editor);
- } else {
- editor.putLong(PlaybackPreferences.PREF_CURRENTLY_PLAYING_MEDIA,
- PlaybackPreferences.NO_MEDIA_PLAYING);
- editor.putLong(PlaybackPreferences.PREF_CURRENTLY_PLAYING_FEED_ID,
- PlaybackPreferences.NO_MEDIA_PLAYING);
- editor.putLong(
- PlaybackPreferences.PREF_CURRENTLY_PLAYING_FEEDMEDIA_ID,
- PlaybackPreferences.NO_MEDIA_PLAYING);
- }
-
- editor.commit();
- }
-
- private void setStatus(PlayerStatus newStatus) {
- if (AppConfig.DEBUG)
- Log.d(TAG, "Setting status to " + newStatus);
- status = newStatus;
- sendBroadcast(new Intent(ACTION_PLAYER_STATUS_CHANGED));
- updateWidget();
- refreshRemoteControlClientState();
- bluetoothNotifyChange();
- }
-
- /** Send ACTION_PLAYER_STATUS_CHANGED without changing the status attribute. */
- private void postStatusUpdateIntent() {
- setStatus(status);
- }
-
- private void sendNotificationBroadcast(int type, int code) {
- Intent intent = new Intent(ACTION_PLAYER_NOTIFICATION);
- intent.putExtra(EXTRA_NOTIFICATION_TYPE, type);
- intent.putExtra(EXTRA_NOTIFICATION_CODE, code);
- sendBroadcast(intent);
- }
-
- /** Used by setupNotification to load notification data in another thread. */
- private AsyncTask notificationSetupTask;
-
- /** Prepares notification and starts the service in the foreground. */
- @SuppressLint("NewApi")
- private void setupNotification() {
- final PendingIntent pIntent = PendingIntent.getActivity(this, 0,
- PlaybackService.getPlayerActivityIntent(this),
- PendingIntent.FLAG_UPDATE_CURRENT);
-
- if (notificationSetupTask != null) {
- notificationSetupTask.cancel(true);
- }
- notificationSetupTask = new AsyncTask() {
- Bitmap icon = null;
-
- @Override
- protected Void doInBackground(Void... params) {
- if (AppConfig.DEBUG)
- Log.d(TAG, "Starting background work");
- if (android.os.Build.VERSION.SDK_INT >= 11) {
- if (media != null && media != null) {
- int iconSize = getResources().getDimensionPixelSize(
- android.R.dimen.notification_large_icon_width);
- icon = BitmapDecoder
- .decodeBitmapFromWorkerTaskResource(iconSize,
- media);
- }
-
- }
- if (icon == null) {
- icon = BitmapFactory.decodeResource(getResources(),
- R.drawable.ic_stat_antenna);
- }
-
- return null;
- }
-
- @Override
- protected void onPostExecute(Void result) {
- super.onPostExecute(result);
- if (!isCancelled() && status == PlayerStatus.PLAYING
- && media != null) {
- String contentText = media.getFeedTitle();
- String contentTitle = media.getEpisodeTitle();
- Notification notification = null;
- if (android.os.Build.VERSION.SDK_INT >= 16) {
- Intent pauseButtonIntent = new Intent(
- PlaybackService.this, PlaybackService.class);
- pauseButtonIntent.putExtra(
- MediaButtonReceiver.EXTRA_KEYCODE,
- KeyEvent.KEYCODE_MEDIA_PAUSE);
- PendingIntent pauseButtonPendingIntent = PendingIntent
- .getService(PlaybackService.this, 0,
- pauseButtonIntent,
- PendingIntent.FLAG_UPDATE_CURRENT);
- Notification.Builder notificationBuilder = new Notification.Builder(
- PlaybackService.this)
- .setContentTitle(contentTitle)
- .setContentText(contentText)
- .setOngoing(true)
- .setContentIntent(pIntent)
- .setLargeIcon(icon)
- .setSmallIcon(R.drawable.ic_stat_antenna)
- .addAction(android.R.drawable.ic_media_pause,
- getString(R.string.pause_label),
- pauseButtonPendingIntent);
- notification = notificationBuilder.build();
- } else {
- NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(
- PlaybackService.this)
- .setContentTitle(contentTitle)
- .setContentText(contentText).setOngoing(true)
- .setContentIntent(pIntent).setLargeIcon(icon)
- .setSmallIcon(R.drawable.ic_stat_antenna);
- notification = notificationBuilder.getNotification();
- }
- startForeground(NOTIFICATION_ID, notification);
- if (AppConfig.DEBUG)
- Log.d(TAG, "Notification set up");
- }
- }
-
- };
- if (android.os.Build.VERSION.SDK_INT > android.os.Build.VERSION_CODES.GINGERBREAD_MR1) {
- notificationSetupTask
- .executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
- } else {
- notificationSetupTask.execute();
- }
-
- }
-
- /**
- * Seek a specific position from the current position
- *
- * @param delta
- * offset from current position (positive or negative)
- * */
- public void seekDelta(int delta) {
- int position = getCurrentPositionSafe();
- if (position != INVALID_TIME) {
- seek(player.getCurrentPosition() + delta);
- }
- }
-
- public void seek(int i) {
- saveCurrentPosition();
- if (status == PlayerStatus.INITIALIZED
- || status == PlayerStatus.INITIALIZING
- || status == PlayerStatus.PREPARING) {
- media.setPosition(i);
- setStartWhenPrepared(true);
- prepare();
- } else {
- if (AppConfig.DEBUG)
- Log.d(TAG, "Seeking position " + i);
- if (shouldStream) {
- if (status != PlayerStatus.SEEKING) {
- statusBeforeSeek = status;
- }
- setStatus(PlayerStatus.SEEKING);
- }
- player.seekTo(i);
- }
- }
-
- public void seekToChapter(Chapter chapter) {
- seek((int) chapter.getStart());
- }
-
- /** Saves the current position of the media file to the DB */
- private synchronized void saveCurrentPosition() {
- int position = getCurrentPositionSafe();
- if (position != INVALID_TIME) {
- if (AppConfig.DEBUG)
- Log.d(TAG, "Saving current position to " + position);
- media.saveCurrentPosition(PreferenceManager
- .getDefaultSharedPreferences(getApplicationContext()),
- position);
- }
- }
-
- private void stopWidgetUpdater() {
- if (widgetUpdaterFuture != null) {
- if (AppConfig.DEBUG)
- Log.d(TAG, "Stopping widgetUpdateWorker");
- widgetUpdaterFuture.cancel(true);
- }
- sendBroadcast(new Intent(PlayerWidget.STOP_WIDGET_UPDATE));
- }
-
- @SuppressLint("NewApi")
- private void setupWidgetUpdater() {
- if (widgetUpdaterFuture == null
- || (widgetUpdaterFuture.isCancelled() || widgetUpdaterFuture
- .isDone())) {
- widgetUpdater = new WidgetUpdateWorker();
- widgetUpdaterFuture = schedExecutor.scheduleAtFixedRate(
- widgetUpdater, WidgetUpdateWorker.NOTIFICATION_INTERVALL,
- WidgetUpdateWorker.NOTIFICATION_INTERVALL,
- TimeUnit.MILLISECONDS);
- }
- }
-
- private void updateWidget() {
- if (AppConfig.DEBUG)
- Log.d(TAG, "Sending widget update request");
- PlaybackService.this.sendBroadcast(new Intent(
- PlayerWidget.FORCE_WIDGET_UPDATE));
- }
-
- public boolean sleepTimerActive() {
- return sleepTimer != null && sleepTimer.isWaiting();
- }
-
- public long getSleepTimerTimeLeft() {
- if (sleepTimerActive()) {
- return sleepTimer.getWaitingTime();
- } else {
- return 0;
- }
- }
-
- @SuppressLint("NewApi")
- private RemoteControlClient setupRemoteControlClient() {
- Intent mediaButtonIntent = new Intent(Intent.ACTION_MEDIA_BUTTON);
- mediaButtonIntent.setComponent(mediaButtonReceiver);
- PendingIntent mediaPendingIntent = PendingIntent.getBroadcast(
- getApplicationContext(), 0, mediaButtonIntent, 0);
- remoteControlClient = new RemoteControlClient(mediaPendingIntent);
- int controlFlags;
- if (android.os.Build.VERSION.SDK_INT < 16) {
- controlFlags = RemoteControlClient.FLAG_KEY_MEDIA_PLAY_PAUSE
- | RemoteControlClient.FLAG_KEY_MEDIA_NEXT;
- } else {
- controlFlags = RemoteControlClient.FLAG_KEY_MEDIA_PLAY_PAUSE;
- }
- remoteControlClient.setTransportControlFlags(controlFlags);
- return remoteControlClient;
- }
-
- /** Refresh player status and metadata. */
- @SuppressLint("NewApi")
- private void refreshRemoteControlClientState() {
- if (android.os.Build.VERSION.SDK_INT >= 14) {
- if (remoteControlClient != null) {
- switch (status) {
- case PLAYING:
- remoteControlClient
- .setPlaybackState(RemoteControlClient.PLAYSTATE_PLAYING);
- break;
- case PAUSED:
- case INITIALIZED:
- remoteControlClient
- .setPlaybackState(RemoteControlClient.PLAYSTATE_PAUSED);
- break;
- case STOPPED:
- remoteControlClient
- .setPlaybackState(RemoteControlClient.PLAYSTATE_STOPPED);
- break;
- case ERROR:
- remoteControlClient
- .setPlaybackState(RemoteControlClient.PLAYSTATE_ERROR);
- break;
- default:
- remoteControlClient
- .setPlaybackState(RemoteControlClient.PLAYSTATE_BUFFERING);
- }
- if (media != null) {
- MetadataEditor editor = remoteControlClient
- .editMetadata(false);
- editor.putString(MediaMetadataRetriever.METADATA_KEY_TITLE,
- media.getEpisodeTitle());
-
- editor.putString(MediaMetadataRetriever.METADATA_KEY_ALBUM,
- media.getFeedTitle());
-
- editor.apply();
- }
- if (AppConfig.DEBUG)
- Log.d(TAG, "RemoteControlClient state was refreshed");
- }
- }
- }
-
- private void bluetoothNotifyChange() {
- boolean isPlaying = false;
-
- if (status == PlayerStatus.PLAYING) {
- isPlaying = true;
- }
-
- if (media != null) {
- Intent i = new Intent(AVRCP_ACTION_PLAYER_STATUS_CHANGED);
- i.putExtra("id", 1);
- i.putExtra("artist", "");
- i.putExtra("album", media.getFeedTitle());
- i.putExtra("track", media.getEpisodeTitle());
- i.putExtra("playing", isPlaying);
- if (queue != null) {
- i.putExtra("ListSize", queue.size());
- }
- i.putExtra("duration", media.getDuration());
- i.putExtra("position", media.getPosition());
- sendBroadcast(i);
- }
- }
-
- /**
- * Pauses playback when the headset is disconnected and the preference is
- * set
- */
- private BroadcastReceiver headsetDisconnected = new BroadcastReceiver() {
- private static final String TAG = "headsetDisconnected";
- private static final int UNPLUGGED = 0;
-
- @Override
- public void onReceive(Context context, Intent intent) {
- if (intent.getAction().equals(Intent.ACTION_HEADSET_PLUG)) {
- int state = intent.getIntExtra("state", -1);
- if (state != -1) {
- if (AppConfig.DEBUG)
- Log.d(TAG, "Headset plug event. State is " + state);
- if (state == UNPLUGGED && status == PlayerStatus.PLAYING) {
- if (AppConfig.DEBUG)
- Log.d(TAG, "Headset was unplugged during playback.");
- pauseIfPauseOnDisconnect();
- }
- } else {
- Log.e(TAG, "Received invalid ACTION_HEADSET_PLUG intent");
- }
- }
- }
- };
-
- private BroadcastReceiver audioBecomingNoisy = new BroadcastReceiver() {
-
- @Override
- public void onReceive(Context context, Intent intent) {
- // sound is about to change, eg. bluetooth -> speaker
- if (AppConfig.DEBUG)
- Log.d(TAG, "Pausing playback because audio is becoming noisy");
- pauseIfPauseOnDisconnect();
- }
- // android.media.AUDIO_BECOMING_NOISY
- };
-
- /** Pauses playback if PREF_PAUSE_ON_HEADSET_DISCONNECT was set to true. */
- private void pauseIfPauseOnDisconnect() {
- if (UserPreferences.isPauseOnHeadsetDisconnect()
- && status == PlayerStatus.PLAYING) {
- pause(true, true);
- }
- }
-
- private BroadcastReceiver shutdownReceiver = new BroadcastReceiver() {
-
- @Override
- public void onReceive(Context context, Intent intent) {
- if (intent.getAction().equals(ACTION_SHUTDOWN_PLAYBACK_SERVICE)) {
- schedExecutor.shutdownNow();
- stop();
- media = null;
- }
- }
-
- };
-
- private BroadcastReceiver skipCurrentEpisodeReceiver = new BroadcastReceiver() {
- @Override
- public void onReceive(Context context, Intent intent) {
- if (intent.getAction().equals(ACTION_SKIP_CURRENT_EPISODE)) {
-
- if (AppConfig.DEBUG)
- Log.d(TAG, "Received SKIP_CURRENT_EPISODE intent");
- if (media != null) {
- setStatus(PlayerStatus.STOPPED);
- endPlayback(true);
- }
- }
- }
- };
-
- /** Periodically saves the position of the media file */
- class PositionSaver implements Runnable {
- public static final int WAITING_INTERVALL = 5000;
-
- @Override
- public void run() {
- if (player != null && player.isPlaying()) {
- try {
- saveCurrentPosition();
- } catch (IllegalStateException e) {
- Log.w(TAG,
- "saveCurrentPosition was called in illegal state");
- }
- }
- }
- }
-
- /** Notifies the player widget in the specified intervall */
- class WidgetUpdateWorker implements Runnable {
- private static final int NOTIFICATION_INTERVALL = 1000;
-
- @Override
- public void run() {
- if (PlaybackService.isRunning) {
- updateWidget();
- }
- }
- }
-
- /** Sleeps for a given time and then pauses playback. */
- class SleepTimer implements Runnable {
- private static final String TAG = "SleepTimer";
- private static final long UPDATE_INTERVALL = 1000L;
- private volatile long waitingTime;
- private boolean isWaiting;
-
- public SleepTimer(long waitingTime) {
- super();
- this.waitingTime = waitingTime;
- }
-
- @Override
- public void run() {
- isWaiting = true;
- if (AppConfig.DEBUG)
- Log.d(TAG, "Starting");
- while (waitingTime > 0) {
- try {
- Thread.sleep(UPDATE_INTERVALL);
- waitingTime -= UPDATE_INTERVALL;
-
- if (waitingTime <= 0) {
- if (AppConfig.DEBUG)
- Log.d(TAG, "Waiting completed");
- if (status == PlayerStatus.PLAYING) {
- if (AppConfig.DEBUG)
- Log.d(TAG, "Pausing playback");
- pause(true, true);
- }
- postExecute();
- }
- } catch (InterruptedException e) {
- Log.d(TAG, "Thread was interrupted while waiting");
- break;
- }
- }
- postExecute();
- }
-
- protected void postExecute() {
- isWaiting = false;
- sendNotificationBroadcast(NOTIFICATION_TYPE_SLEEPTIMER_UPDATE, 0);
- }
-
- public long getWaitingTime() {
- return waitingTime;
- }
-
- public boolean isWaiting() {
- return isWaiting;
- }
-
- }
-
- public static boolean isPlayingVideo() {
- return playingVideo;
- }
-
- public boolean isShouldStream() {
- return shouldStream;
- }
-
- public PlayerStatus getStatus() {
- return status;
- }
-
- public Playable getMedia() {
- return media;
- }
-
- public IPlayer getPlayer() {
- return player;
- }
-
- public boolean isStartWhenPrepared() {
- return startWhenPrepared;
- }
-
- public void setStartWhenPrepared(boolean startWhenPrepared) {
- this.startWhenPrepared = startWhenPrepared;
- postStatusUpdateIntent();
- }
-
- public boolean canSetSpeed() {
- if (player != null && media != null && media.getMediaType() == MediaType.AUDIO) {
- return ((AudioPlayer) player).canSetSpeed();
- }
- return false;
- }
-
- public boolean canSetPitch() {
- if (player != null && media != null && media.getMediaType() == MediaType.AUDIO) {
- return ((AudioPlayer) player).canSetPitch();
- }
- return false;
- }
-
- public void setSpeed(float speed) {
- if (media != null && media.getMediaType() == MediaType.AUDIO) {
- AudioPlayer audioPlayer = (AudioPlayer) player;
- if (audioPlayer.canSetSpeed()) {
- audioPlayer.setPlaybackSpeed((float) speed);
- if (AppConfig.DEBUG)
- Log.d(TAG, "Playback speed was set to " + speed);
- sendNotificationBroadcast(
- NOTIFICATION_TYPE_PLAYBACK_SPEED_CHANGE, 0);
- }
- }
- }
-
- public void setPitch(float pitch) {
- if (media != null && media.getMediaType() == MediaType.AUDIO) {
- AudioPlayer audioPlayer = (AudioPlayer) player;
- if (audioPlayer.canSetPitch()) {
- audioPlayer.setPlaybackPitch((float) pitch);
- }
- }
- }
-
- public float getCurrentPlaybackSpeed() {
- if (media.getMediaType() == MediaType.AUDIO
- && player instanceof AudioPlayer) {
- AudioPlayer audioPlayer = (AudioPlayer) player;
- if (audioPlayer.canSetSpeed()) {
- return audioPlayer.getCurrentSpeedMultiplier();
- }
- }
- return -1;
- }
-
- /**
- * call getDuration() on mediaplayer or return INVALID_TIME if player is in
- * an invalid state. This method should be used instead of calling
- * getDuration() directly to avoid an error.
- */
- public int getDurationSafe() {
- if (status != null && player != null) {
- switch (status) {
- case PREPARED:
- case PLAYING:
- case PAUSED:
- case SEEKING:
- try {
- return player.getDuration();
- } catch (IllegalStateException e) {
- e.printStackTrace();
- return INVALID_TIME;
- }
- default:
- return INVALID_TIME;
- }
- } else {
- return INVALID_TIME;
- }
- }
-
- /**
- * call getCurrentPosition() on mediaplayer or return INVALID_TIME if player
- * is in an invalid state. This method should be used instead of calling
- * getCurrentPosition() directly to avoid an error.
- */
- public int getCurrentPositionSafe() {
- if (status != null && player != null) {
- switch (status) {
- case PREPARED:
- case PLAYING:
- case PAUSED:
- case SEEKING:
- return player.getCurrentPosition();
- default:
- return INVALID_TIME;
- }
- } else {
- return INVALID_TIME;
- }
- }
-
- private void setCurrentlyPlayingMedia(long id) {
- SharedPreferences.Editor editor = PreferenceManager
- .getDefaultSharedPreferences(getApplicationContext()).edit();
- editor.putLong(PlaybackPreferences.PREF_CURRENTLY_PLAYING_MEDIA, id);
- editor.commit();
- }
-
- private static class InitTask extends AsyncTask {
- private Playable playable;
- public PlayableException exception;
-
- @Override
- protected Playable doInBackground(Playable... params) {
- if (params[0] == null) {
- throw new IllegalArgumentException("Playable must not be null");
- }
- playable = params[0];
-
- try {
- playable.loadMetadata();
- } catch (PlayableException e) {
- e.printStackTrace();
- exception = e;
- return null;
- }
- return playable;
- }
-
- @SuppressLint("NewApi")
- public void executeAsync(Playable playable) {
- FlattrUtils.hasToken();
- if (android.os.Build.VERSION.SDK_INT > android.os.Build.VERSION_CODES.GINGERBREAD_MR1) {
- executeOnExecutor(THREAD_POOL_EXECUTOR, playable);
- } else {
- execute(playable);
- }
- }
-
- }
-
- private void loadQueue() {
- dbLoaderExecutor.submit(new QueueLoaderTask());
- }
-
- private class QueueLoaderTask implements Runnable {
- @Override
- public void run() {
- List queueRef = DBReader.getQueue(PlaybackService.this);
- queue = queueRef;
- }
- }
-}
diff --git a/src/de/danoeh/antennapod/service/playback/PlaybackService.java b/src/de/danoeh/antennapod/service/playback/PlaybackService.java
new file mode 100644
index 000000000..21aca915e
--- /dev/null
+++ b/src/de/danoeh/antennapod/service/playback/PlaybackService.java
@@ -0,0 +1,1006 @@
+package de.danoeh.antennapod.service.playback;
+
+import android.annotation.SuppressLint;
+import android.app.Notification;
+import android.app.PendingIntent;
+import android.app.Service;
+import android.content.*;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.media.AudioManager;
+import android.media.MediaMetadataRetriever;
+import android.media.MediaPlayer;
+import android.media.RemoteControlClient;
+import android.media.RemoteControlClient.MetadataEditor;
+import android.os.AsyncTask;
+import android.os.Binder;
+import android.os.Build;
+import android.os.IBinder;
+import android.preference.PreferenceManager;
+import android.support.v4.app.NotificationCompat;
+import android.util.Log;
+import android.util.Pair;
+import android.view.KeyEvent;
+import android.view.SurfaceHolder;
+import de.danoeh.antennapod.AppConfig;
+import de.danoeh.antennapod.R;
+import de.danoeh.antennapod.activity.AudioplayerActivity;
+import de.danoeh.antennapod.activity.VideoplayerActivity;
+import de.danoeh.antennapod.feed.Chapter;
+import de.danoeh.antennapod.feed.FeedItem;
+import de.danoeh.antennapod.feed.FeedMedia;
+import de.danoeh.antennapod.feed.MediaType;
+import de.danoeh.antennapod.preferences.PlaybackPreferences;
+import de.danoeh.antennapod.preferences.UserPreferences;
+import de.danoeh.antennapod.receiver.MediaButtonReceiver;
+import de.danoeh.antennapod.receiver.PlayerWidget;
+import de.danoeh.antennapod.storage.DBTasks;
+import de.danoeh.antennapod.storage.DBWriter;
+import de.danoeh.antennapod.util.BitmapDecoder;
+import de.danoeh.antennapod.util.QueueAccess;
+import de.danoeh.antennapod.util.playback.Playable;
+import de.danoeh.antennapod.util.playback.PlaybackController;
+
+import java.util.List;
+
+/**
+ * Controls the MediaPlayer that plays a FeedMedia-file
+ */
+public class PlaybackService extends Service {
+ /**
+ * Logging tag
+ */
+ private static final String TAG = "PlaybackService";
+
+ /**
+ * Parcelable of type Playable.
+ */
+ public static final String EXTRA_PLAYABLE = "PlaybackService.PlayableExtra";
+ /**
+ * True if media should be streamed.
+ */
+ public static final String EXTRA_SHOULD_STREAM = "extra.de.danoeh.antennapod.service.shouldStream";
+ /**
+ * True if playback should be started immediately after media has been
+ * prepared.
+ */
+ public static final String EXTRA_START_WHEN_PREPARED = "extra.de.danoeh.antennapod.service.startWhenPrepared";
+
+ public static final String EXTRA_PREPARE_IMMEDIATELY = "extra.de.danoeh.antennapod.service.prepareImmediately";
+
+ public static final String ACTION_PLAYER_STATUS_CHANGED = "action.de.danoeh.antennapod.service.playerStatusChanged";
+ private static final String AVRCP_ACTION_PLAYER_STATUS_CHANGED = "com.android.music.playstatechanged";
+
+ public static final String ACTION_PLAYER_NOTIFICATION = "action.de.danoeh.antennapod.service.playerNotification";
+ public static final String EXTRA_NOTIFICATION_CODE = "extra.de.danoeh.antennapod.service.notificationCode";
+ public static final String EXTRA_NOTIFICATION_TYPE = "extra.de.danoeh.antennapod.service.notificationType";
+
+ /**
+ * If the PlaybackService receives this action, it will stop playback and
+ * try to shutdown.
+ */
+ public static final String ACTION_SHUTDOWN_PLAYBACK_SERVICE = "action.de.danoeh.antennapod.service.actionShutdownPlaybackService";
+
+ /**
+ * If the PlaybackService receives this action, it will end playback of the
+ * current episode and load the next episode if there is one available.
+ */
+ public static final String ACTION_SKIP_CURRENT_EPISODE = "action.de.danoeh.antennapod.service.skipCurrentEpisode";
+
+ /**
+ * Used in NOTIFICATION_TYPE_RELOAD.
+ */
+ public static final int EXTRA_CODE_AUDIO = 1;
+ public static final int EXTRA_CODE_VIDEO = 2;
+
+ public static final int NOTIFICATION_TYPE_ERROR = 0;
+ public static final int NOTIFICATION_TYPE_INFO = 1;
+ public static final int NOTIFICATION_TYPE_BUFFER_UPDATE = 2;
+
+ /**
+ * Receivers of this intent should update their information about the curently playing media
+ */
+ public static final int NOTIFICATION_TYPE_RELOAD = 3;
+ /**
+ * The state of the sleeptimer changed.
+ */
+ public static final int NOTIFICATION_TYPE_SLEEPTIMER_UPDATE = 4;
+ public static final int NOTIFICATION_TYPE_BUFFER_START = 5;
+ public static final int NOTIFICATION_TYPE_BUFFER_END = 6;
+ /**
+ * No more episodes are going to be played.
+ */
+ public static final int NOTIFICATION_TYPE_PLAYBACK_END = 7;
+
+ /**
+ * Playback speed has changed
+ */
+ public static final int NOTIFICATION_TYPE_PLAYBACK_SPEED_CHANGE = 8;
+
+ /**
+ * Returned by getPositionSafe() or getDurationSafe() if the playbackService
+ * is in an invalid state.
+ */
+ public static final int INVALID_TIME = -1;
+
+ /**
+ * Is true if service is running.
+ */
+ public static boolean isRunning = false;
+
+ private static final int NOTIFICATION_ID = 1;
+
+ private RemoteControlClient remoteControlClient;
+ private PlaybackServiceMediaPlayer mediaPlayer;
+ private PlaybackServiceTaskManager taskManager;
+
+ private static volatile MediaType currentMediaType = MediaType.UNKNOWN;
+
+ private final IBinder mBinder = new LocalBinder();
+
+ public class LocalBinder extends Binder {
+ public PlaybackService getService() {
+ return PlaybackService.this;
+ }
+ }
+
+ @Override
+ public boolean onUnbind(Intent intent) {
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Received onUnbind event");
+ return super.onUnbind(intent);
+ }
+
+ /**
+ * Returns an intent which starts an audio- or videoplayer, depending on the
+ * type of media that is being played. If the playbackservice is not
+ * running, the type of the last played media will be looked up.
+ */
+ public static Intent getPlayerActivityIntent(Context context) {
+ if (isRunning) {
+ if (currentMediaType == MediaType.VIDEO) {
+ return new Intent(context, VideoplayerActivity.class);
+ } else {
+ return new Intent(context, AudioplayerActivity.class);
+ }
+ } else {
+ if (PlaybackPreferences.getCurrentEpisodeIsVideo()) {
+ return new Intent(context, VideoplayerActivity.class);
+ } else {
+ return new Intent(context, AudioplayerActivity.class);
+ }
+ }
+ }
+
+ /**
+ * Same as getPlayerActivityIntent(context), but here the type of activity
+ * depends on the FeedMedia that is provided as an argument.
+ */
+ public static Intent getPlayerActivityIntent(Context context, Playable media) {
+ MediaType mt = media.getMediaType();
+ if (mt == MediaType.VIDEO) {
+ return new Intent(context, VideoplayerActivity.class);
+ } else {
+ return new Intent(context, AudioplayerActivity.class);
+ }
+ }
+
+ @SuppressLint("NewApi")
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Service created.");
+ isRunning = true;
+
+ registerReceiver(headsetDisconnected, new IntentFilter(
+ Intent.ACTION_HEADSET_PLUG));
+ registerReceiver(shutdownReceiver, new IntentFilter(
+ ACTION_SHUTDOWN_PLAYBACK_SERVICE));
+ registerReceiver(audioBecomingNoisy, new IntentFilter(
+ AudioManager.ACTION_AUDIO_BECOMING_NOISY));
+ registerReceiver(skipCurrentEpisodeReceiver, new IntentFilter(
+ ACTION_SKIP_CURRENT_EPISODE));
+ remoteControlClient = setupRemoteControlClient();
+ taskManager = new PlaybackServiceTaskManager(this, taskManagerCallback);
+ mediaPlayer = new PlaybackServiceMediaPlayer(this, mediaPlayerCallback);
+
+ }
+
+ @SuppressLint("NewApi")
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Service is about to be destroyed");
+ isRunning = false;
+ currentMediaType = MediaType.UNKNOWN;
+
+ unregisterReceiver(headsetDisconnected);
+ unregisterReceiver(shutdownReceiver);
+ unregisterReceiver(audioBecomingNoisy);
+ unregisterReceiver(skipCurrentEpisodeReceiver);
+ mediaPlayer.shutdown();
+ taskManager.shutdown();
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Received onBind event");
+ return mBinder;
+ }
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ super.onStartCommand(intent, flags, startId);
+
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "OnStartCommand called");
+ final int keycode = intent.getIntExtra(MediaButtonReceiver.EXTRA_KEYCODE, -1);
+ final Playable playable = intent.getParcelableExtra(EXTRA_PLAYABLE);
+ if (keycode == -1 && playable == null) {
+ Log.e(TAG, "PlaybackService was started with no arguments");
+ stopSelf();
+ }
+
+ if ((flags & Service.START_FLAG_REDELIVERY) != 0) {
+ if (AppConfig.DEBUG) Log.d(TAG, "onStartCommand is a redelivered intent, calling stopForeground now.");
+ stopForeground(true);
+ }
+
+ if (keycode != -1) {
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Received media button event");
+ handleKeycode(keycode);
+ } else {
+ boolean stream = intent.getBooleanExtra(EXTRA_SHOULD_STREAM,
+ true);
+ boolean startWhenPrepared = intent.getBooleanExtra(EXTRA_START_WHEN_PREPARED, false);
+ boolean prepareImmediately = intent.getBooleanExtra(EXTRA_PREPARE_IMMEDIATELY, false);
+ sendNotificationBroadcast(NOTIFICATION_TYPE_RELOAD, 0);
+ mediaPlayer.playMediaObject(playable, stream, startWhenPrepared, prepareImmediately);
+ }
+
+ return Service.START_REDELIVER_INTENT;
+ }
+
+ /**
+ * Handles media button events
+ */
+ private void handleKeycode(int keycode) {
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Handling keycode: " + keycode);
+
+ final PlayerStatus status = mediaPlayer.getPSMPInfo().playerStatus;
+ switch (keycode) {
+ case KeyEvent.KEYCODE_HEADSETHOOK:
+ case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE:
+ if (status == PlayerStatus.PLAYING) {
+ mediaPlayer.pause(true, true);
+ } else if (status == PlayerStatus.PAUSED || status == PlayerStatus.PREPARED) {
+ mediaPlayer.resume();
+ } else if (status == PlayerStatus.PREPARING) {
+ mediaPlayer.setStartWhenPrepared(!mediaPlayer.isStartWhenPrepared());
+ } else if (status == PlayerStatus.INITIALIZED) {
+ mediaPlayer.setStartWhenPrepared(true);
+ mediaPlayer.prepare();
+ }
+ break;
+ case KeyEvent.KEYCODE_MEDIA_PLAY:
+ if (status == PlayerStatus.PAUSED || status == PlayerStatus.PREPARED) {
+ mediaPlayer.resume();
+ } else if (status == PlayerStatus.INITIALIZED) {
+ mediaPlayer.setStartWhenPrepared(true);
+ mediaPlayer.prepare();
+ }
+ break;
+ case KeyEvent.KEYCODE_MEDIA_PAUSE:
+ if (status == PlayerStatus.PLAYING) {
+ mediaPlayer.pause(true, true);
+ }
+ break;
+ case KeyEvent.KEYCODE_MEDIA_FAST_FORWARD: {
+ mediaPlayer.seekDelta(PlaybackController.DEFAULT_SEEK_DELTA);
+ break;
+ }
+ case KeyEvent.KEYCODE_MEDIA_REWIND: {
+ mediaPlayer.seekDelta(-PlaybackController.DEFAULT_SEEK_DELTA);
+ break;
+ }
+ }
+ }
+
+ /**
+ * Called by a mediaplayer Activity as soon as it has prepared its
+ * mediaplayer.
+ */
+ public void setVideoSurface(SurfaceHolder sh) {
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Setting display");
+ mediaPlayer.setVideoSurface(sh);
+ }
+
+ /**
+ * Called when the surface holder of the mediaplayer has to be changed.
+ */
+ private void resetVideoSurface() {
+ taskManager.cancelPositionSaver();
+ mediaPlayer.resetVideoSurface();
+ }
+
+ public void notifyVideoSurfaceAbandoned() {
+ stopForeground(true);
+ mediaPlayer.resetVideoSurface();
+ }
+
+ private final PlaybackServiceTaskManager.PSTMCallback taskManagerCallback = new PlaybackServiceTaskManager.PSTMCallback() {
+ @Override
+ public void positionSaverTick() {
+ saveCurrentPosition();
+ }
+
+ @Override
+ public void onSleepTimerExpired() {
+ mediaPlayer.pause(true, true);
+ sendNotificationBroadcast(NOTIFICATION_TYPE_SLEEPTIMER_UPDATE, 0);
+ }
+
+
+ @Override
+ public void onWidgetUpdaterTick() {
+ updateWidget();
+ }
+
+ @Override
+ public void onChapterLoaded(Playable media) {
+ sendNotificationBroadcast(NOTIFICATION_TYPE_RELOAD, 0);
+ }
+ };
+
+ private final PlaybackServiceMediaPlayer.PSMPCallback mediaPlayerCallback = new PlaybackServiceMediaPlayer.PSMPCallback() {
+ @Override
+ public void statusChanged(PlaybackServiceMediaPlayer.PSMPInfo newInfo) {
+ currentMediaType = mediaPlayer.getCurrentMediaType();
+ switch (newInfo.playerStatus) {
+ case INITIALIZED:
+ writePlaybackPreferences();
+ break;
+
+ case PREPARED:
+ taskManager.startChapterLoader(newInfo.playable);
+ break;
+
+ case PAUSED:
+ taskManager.cancelPositionSaver();
+ saveCurrentPosition();
+ taskManager.cancelWidgetUpdater();
+ stopForeground(true);
+ break;
+
+ case STOPPED:
+ //setCurrentlyPlayingMedia(PlaybackPreferences.NO_MEDIA_PLAYING);
+ //stopSelf();
+ break;
+
+ case PLAYING:
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Audiofocus successfully requested");
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Resuming/Starting playback");
+
+ taskManager.startPositionSaver();
+ taskManager.startWidgetUpdater();
+ setupNotification(newInfo);
+ break;
+
+ }
+
+ sendBroadcast(new Intent(ACTION_PLAYER_STATUS_CHANGED));
+ updateWidget();
+ refreshRemoteControlClientState(newInfo);
+ bluetoothNotifyChange(newInfo);
+ }
+
+ @Override
+ public void shouldStop() {
+ stopSelf();
+ }
+
+ @Override
+ public void playbackSpeedChanged(float s) {
+ sendNotificationBroadcast(
+ NOTIFICATION_TYPE_PLAYBACK_SPEED_CHANGE, 0);
+ }
+
+ @Override
+ public void onBufferingUpdate(int percent) {
+ sendNotificationBroadcast(NOTIFICATION_TYPE_BUFFER_UPDATE, percent);
+ }
+
+ @Override
+ public boolean onMediaPlayerInfo(int code) {
+ switch (code) {
+ case MediaPlayer.MEDIA_INFO_BUFFERING_START:
+ sendNotificationBroadcast(NOTIFICATION_TYPE_BUFFER_START, 0);
+ return true;
+ case MediaPlayer.MEDIA_INFO_BUFFERING_END:
+ sendNotificationBroadcast(NOTIFICATION_TYPE_BUFFER_END, 0);
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ @Override
+ public boolean onMediaPlayerError(Object inObj, int what, int extra) {
+ final String TAG = "PlaybackService.onErrorListener";
+ Log.w(TAG, "An error has occured: " + what + " " + extra);
+ if (mediaPlayer.getPSMPInfo().playerStatus == PlayerStatus.PLAYING) {
+ mediaPlayer.pause(true, false);
+ }
+ sendNotificationBroadcast(NOTIFICATION_TYPE_ERROR, what);
+ setCurrentlyPlayingMedia(PlaybackPreferences.NO_MEDIA_PLAYING);
+ stopSelf();
+ return true;
+ }
+
+ @Override
+ public boolean endPlayback(boolean playNextEpisode) {
+ PlaybackService.this.endPlayback(true);
+ return true;
+ }
+
+ @Override
+ public RemoteControlClient getRemoteControlClient() {
+ return remoteControlClient;
+ }
+ };
+
+ private void endPlayback(boolean playNextEpisode) {
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Playback ended");
+
+ final Playable media = mediaPlayer.getPSMPInfo().playable;
+ if (media == null) {
+ Log.e(TAG, "Cannot end playback: media was null");
+ return;
+ }
+
+ taskManager.cancelPositionSaver();
+
+ boolean isInQueue = false;
+ FeedItem nextItem = null;
+
+ if (media instanceof FeedMedia) {
+ FeedItem item = ((FeedMedia) media).getItem();
+ DBWriter.markItemRead(PlaybackService.this, item, true, true);
+
+ try {
+ final List queue = taskManager.getQueue();
+ isInQueue = QueueAccess.ItemListAccess(queue).contains(((FeedMedia) media).getItem().getId());
+ nextItem = DBTasks.getQueueSuccessorOfItem(this, item.getId(), queue);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ // isInQueue remains false
+ }
+ if (isInQueue) {
+ DBWriter.removeQueueItem(PlaybackService.this, item.getId(), true);
+ }
+ DBWriter.addItemToPlaybackHistory(PlaybackService.this, (FeedMedia) media);
+ }
+
+ // Load next episode if previous episode was in the queue and if there
+ // is an episode in the queue left.
+ // Start playback immediately if continuous playback is enabled
+ Playable nextMedia = null;
+ boolean loadNextItem = isInQueue && nextItem != null;
+ playNextEpisode = playNextEpisode && loadNextItem
+ && UserPreferences.isFollowQueue();
+ if (loadNextItem) {
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Loading next item in queue");
+ nextMedia = nextItem.getMedia();
+ }
+ final boolean prepareImmediately;
+ final boolean startWhenPrepared;
+ final boolean stream;
+
+ if (playNextEpisode) {
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Playback of next episode will start immediately.");
+ prepareImmediately = startWhenPrepared = true;
+ } else {
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "No more episodes available to play");
+
+ prepareImmediately = startWhenPrepared = false;
+ stopForeground(true);
+ stopWidgetUpdater();
+ }
+
+ writePlaybackPreferences();
+ if (nextMedia != null) {
+ stream = !media.localFileAvailable();
+ mediaPlayer.playMediaObject(nextMedia, stream, startWhenPrepared, prepareImmediately);
+ sendNotificationBroadcast(NOTIFICATION_TYPE_RELOAD,
+ (nextMedia.getMediaType() == MediaType.VIDEO) ? EXTRA_CODE_VIDEO : EXTRA_CODE_AUDIO);
+ } else {
+ sendNotificationBroadcast(NOTIFICATION_TYPE_PLAYBACK_END, 0);
+ //stopSelf();
+ }
+ }
+
+ public void setSleepTimer(long waitingTime) {
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Setting sleep timer to " + Long.toString(waitingTime)
+ + " milliseconds");
+ taskManager.setSleepTimer(waitingTime);
+ sendNotificationBroadcast(NOTIFICATION_TYPE_SLEEPTIMER_UPDATE, 0);
+ }
+
+ public void disableSleepTimer() {
+ taskManager.disableSleepTimer();
+ sendNotificationBroadcast(NOTIFICATION_TYPE_SLEEPTIMER_UPDATE, 0);
+ }
+
+
+ private void writePlaybackPreferences() {
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Writing playback preferences");
+
+ SharedPreferences.Editor editor = PreferenceManager
+ .getDefaultSharedPreferences(getApplicationContext()).edit();
+ PlaybackServiceMediaPlayer.PSMPInfo info = mediaPlayer.getPSMPInfo();
+ MediaType mediaType = mediaPlayer.getCurrentMediaType();
+ boolean stream = mediaPlayer.isStreaming();
+
+ if (info.playable != null) {
+ editor.putLong(PlaybackPreferences.PREF_CURRENTLY_PLAYING_MEDIA,
+ info.playable.getPlayableType());
+ editor.putBoolean(
+ PlaybackPreferences.PREF_CURRENT_EPISODE_IS_STREAM,
+ stream);
+ editor.putBoolean(
+ PlaybackPreferences.PREF_CURRENT_EPISODE_IS_VIDEO,
+ mediaType == MediaType.VIDEO);
+ if (info.playable instanceof FeedMedia) {
+ FeedMedia fMedia = (FeedMedia) info.playable;
+ editor.putLong(
+ PlaybackPreferences.PREF_CURRENTLY_PLAYING_FEED_ID,
+ fMedia.getItem().getFeed().getId());
+ editor.putLong(
+ PlaybackPreferences.PREF_CURRENTLY_PLAYING_FEEDMEDIA_ID,
+ fMedia.getId());
+ } else {
+ editor.putLong(
+ PlaybackPreferences.PREF_CURRENTLY_PLAYING_FEED_ID,
+ PlaybackPreferences.NO_MEDIA_PLAYING);
+ editor.putLong(
+ PlaybackPreferences.PREF_CURRENTLY_PLAYING_FEEDMEDIA_ID,
+ PlaybackPreferences.NO_MEDIA_PLAYING);
+ }
+ info.playable.writeToPreferences(editor);
+ } else {
+ editor.putLong(PlaybackPreferences.PREF_CURRENTLY_PLAYING_MEDIA,
+ PlaybackPreferences.NO_MEDIA_PLAYING);
+ editor.putLong(PlaybackPreferences.PREF_CURRENTLY_PLAYING_FEED_ID,
+ PlaybackPreferences.NO_MEDIA_PLAYING);
+ editor.putLong(
+ PlaybackPreferences.PREF_CURRENTLY_PLAYING_FEEDMEDIA_ID,
+ PlaybackPreferences.NO_MEDIA_PLAYING);
+ }
+
+ editor.commit();
+ }
+
+ /**
+ * Send ACTION_PLAYER_STATUS_CHANGED without changing the status attribute.
+ */
+ private void postStatusUpdateIntent() {
+ sendBroadcast(new Intent(ACTION_PLAYER_STATUS_CHANGED));
+ }
+
+ private void sendNotificationBroadcast(int type, int code) {
+ Intent intent = new Intent(ACTION_PLAYER_NOTIFICATION);
+ intent.putExtra(EXTRA_NOTIFICATION_TYPE, type);
+ intent.putExtra(EXTRA_NOTIFICATION_CODE, code);
+ sendBroadcast(intent);
+ }
+
+ /**
+ * Used by setupNotification to load notification data in another thread.
+ */
+ private AsyncTask notificationSetupTask;
+
+ /**
+ * Prepares notification and starts the service in the foreground.
+ */
+ @SuppressLint("NewApi")
+ private void setupNotification(final PlaybackServiceMediaPlayer.PSMPInfo info) {
+ final PendingIntent pIntent = PendingIntent.getActivity(this, 0,
+ PlaybackService.getPlayerActivityIntent(this),
+ PendingIntent.FLAG_UPDATE_CURRENT);
+
+ if (notificationSetupTask != null) {
+ notificationSetupTask.cancel(true);
+ }
+ notificationSetupTask = new AsyncTask() {
+ Bitmap icon = null;
+
+ @Override
+ protected Void doInBackground(Void... params) {
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Starting background work");
+ if (android.os.Build.VERSION.SDK_INT >= 11) {
+ if (info.playable != null) {
+ int iconSize = getResources().getDimensionPixelSize(
+ android.R.dimen.notification_large_icon_width);
+ icon = BitmapDecoder
+ .decodeBitmapFromWorkerTaskResource(iconSize,
+ info.playable);
+ }
+
+ }
+ if (icon == null) {
+ icon = BitmapFactory.decodeResource(getResources(),
+ R.drawable.ic_stat_antenna);
+ }
+
+ return null;
+ }
+
+ @Override
+ protected void onPostExecute(Void result) {
+ super.onPostExecute(result);
+ if (!isCancelled() && info.playerStatus == PlayerStatus.PLAYING
+ && info.playable != null) {
+ String contentText = info.playable.getFeedTitle();
+ String contentTitle = info.playable.getEpisodeTitle();
+ Notification notification = null;
+ if (android.os.Build.VERSION.SDK_INT >= 16) {
+ Intent pauseButtonIntent = new Intent(
+ PlaybackService.this, PlaybackService.class);
+ pauseButtonIntent.putExtra(
+ MediaButtonReceiver.EXTRA_KEYCODE,
+ KeyEvent.KEYCODE_MEDIA_PAUSE);
+ PendingIntent pauseButtonPendingIntent = PendingIntent
+ .getService(PlaybackService.this, 0,
+ pauseButtonIntent,
+ PendingIntent.FLAG_UPDATE_CURRENT);
+ Notification.Builder notificationBuilder = new Notification.Builder(
+ PlaybackService.this)
+ .setContentTitle(contentTitle)
+ .setContentText(contentText)
+ .setOngoing(true)
+ .setContentIntent(pIntent)
+ .setLargeIcon(icon)
+ .setSmallIcon(R.drawable.ic_stat_antenna)
+ .addAction(android.R.drawable.ic_media_pause,
+ getString(R.string.pause_label),
+ pauseButtonPendingIntent);
+ notification = notificationBuilder.build();
+ } else {
+ NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(
+ PlaybackService.this)
+ .setContentTitle(contentTitle)
+ .setContentText(contentText).setOngoing(true)
+ .setContentIntent(pIntent).setLargeIcon(icon)
+ .setSmallIcon(R.drawable.ic_stat_antenna);
+ notification = notificationBuilder.getNotification();
+ }
+ startForeground(NOTIFICATION_ID, notification);
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Notification set up");
+ }
+ }
+
+ };
+ if (android.os.Build.VERSION.SDK_INT > android.os.Build.VERSION_CODES.GINGERBREAD_MR1) {
+ notificationSetupTask
+ .executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ } else {
+ notificationSetupTask.execute();
+ }
+
+ }
+
+ /**
+ * Saves the current position of the media file to the DB
+ */
+ private synchronized void saveCurrentPosition() {
+ int position = getCurrentPosition();
+ final Playable playable = mediaPlayer.getPSMPInfo().playable;
+ if (position != INVALID_TIME && playable != null) {
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Saving current position to " + position);
+ playable.saveCurrentPosition(PreferenceManager
+ .getDefaultSharedPreferences(getApplicationContext()),
+ position);
+ }
+ }
+
+ private void stopWidgetUpdater() {
+ taskManager.cancelWidgetUpdater();
+ sendBroadcast(new Intent(PlayerWidget.STOP_WIDGET_UPDATE));
+ }
+
+ private void updateWidget() {
+ PlaybackService.this.sendBroadcast(new Intent(
+ PlayerWidget.FORCE_WIDGET_UPDATE));
+ }
+
+ public boolean sleepTimerActive() {
+ return taskManager.isSleepTimerActive();
+ }
+
+ public long getSleepTimerTimeLeft() {
+ return taskManager.getSleepTimerTimeLeft();
+ }
+
+ @SuppressLint("NewApi")
+ private RemoteControlClient setupRemoteControlClient() {
+ if (Build.VERSION.SDK_INT < 14) {
+ return null;
+ }
+
+ Intent mediaButtonIntent = new Intent(Intent.ACTION_MEDIA_BUTTON);
+ mediaButtonIntent.setComponent(new ComponentName(getPackageName(),
+ MediaButtonReceiver.class.getName()));
+ PendingIntent mediaPendingIntent = PendingIntent.getBroadcast(
+ getApplicationContext(), 0, mediaButtonIntent, 0);
+ remoteControlClient = new RemoteControlClient(mediaPendingIntent);
+ int controlFlags;
+ if (android.os.Build.VERSION.SDK_INT < 16) {
+ controlFlags = RemoteControlClient.FLAG_KEY_MEDIA_PLAY_PAUSE
+ | RemoteControlClient.FLAG_KEY_MEDIA_NEXT;
+ } else {
+ controlFlags = RemoteControlClient.FLAG_KEY_MEDIA_PLAY_PAUSE;
+ }
+ remoteControlClient.setTransportControlFlags(controlFlags);
+ return remoteControlClient;
+ }
+
+ /**
+ * Refresh player status and metadata.
+ */
+ @SuppressLint("NewApi")
+ private void refreshRemoteControlClientState(PlaybackServiceMediaPlayer.PSMPInfo info) {
+ if (android.os.Build.VERSION.SDK_INT >= 14) {
+ if (remoteControlClient != null) {
+ switch (info.playerStatus) {
+ case PLAYING:
+ remoteControlClient
+ .setPlaybackState(RemoteControlClient.PLAYSTATE_PLAYING);
+ break;
+ case PAUSED:
+ case INITIALIZED:
+ remoteControlClient
+ .setPlaybackState(RemoteControlClient.PLAYSTATE_PAUSED);
+ break;
+ case STOPPED:
+ remoteControlClient
+ .setPlaybackState(RemoteControlClient.PLAYSTATE_STOPPED);
+ break;
+ case ERROR:
+ remoteControlClient
+ .setPlaybackState(RemoteControlClient.PLAYSTATE_ERROR);
+ break;
+ default:
+ remoteControlClient
+ .setPlaybackState(RemoteControlClient.PLAYSTATE_BUFFERING);
+ }
+ if (info.playable != null) {
+ MetadataEditor editor = remoteControlClient
+ .editMetadata(false);
+ editor.putString(MediaMetadataRetriever.METADATA_KEY_TITLE,
+ info.playable.getEpisodeTitle());
+
+ editor.putString(MediaMetadataRetriever.METADATA_KEY_ALBUM,
+ info.playable.getFeedTitle());
+
+ editor.apply();
+ }
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "RemoteControlClient state was refreshed");
+ }
+ }
+ }
+
+ private void bluetoothNotifyChange(PlaybackServiceMediaPlayer.PSMPInfo info) {
+ boolean isPlaying = false;
+
+ if (info.playerStatus == PlayerStatus.PLAYING) {
+ isPlaying = true;
+ }
+
+ if (info.playable != null) {
+ Intent i = new Intent(AVRCP_ACTION_PLAYER_STATUS_CHANGED);
+ i.putExtra("id", 1);
+ i.putExtra("artist", "");
+ i.putExtra("album", info.playable.getFeedTitle());
+ i.putExtra("track", info.playable.getEpisodeTitle());
+ i.putExtra("playing", isPlaying);
+ final List queue = taskManager.getQueueIfLoaded();
+ if (queue != null) {
+ i.putExtra("ListSize", queue.size());
+ }
+ i.putExtra("duration", info.playable.getDuration());
+ i.putExtra("position", info.playable.getPosition());
+ sendBroadcast(i);
+ }
+ }
+
+ /**
+ * Pauses playback when the headset is disconnected and the preference is
+ * set
+ */
+ private BroadcastReceiver headsetDisconnected = new BroadcastReceiver() {
+ private static final String TAG = "headsetDisconnected";
+ private static final int UNPLUGGED = 0;
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (intent.getAction() != null &&
+ intent.getAction().equals(Intent.ACTION_HEADSET_PLUG)) {
+ int state = intent.getIntExtra("state", -1);
+ if (state != -1) {
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Headset plug event. State is " + state);
+ if (state == UNPLUGGED) {
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Headset was unplugged during playback.");
+ pauseIfPauseOnDisconnect();
+ }
+ } else {
+ Log.e(TAG, "Received invalid ACTION_HEADSET_PLUG intent");
+ }
+ }
+ }
+ };
+
+ private BroadcastReceiver audioBecomingNoisy = new BroadcastReceiver() {
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ // sound is about to change, eg. bluetooth -> speaker
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Pausing playback because audio is becoming noisy");
+ pauseIfPauseOnDisconnect();
+ }
+ // android.media.AUDIO_BECOMING_NOISY
+ };
+
+ /**
+ * Pauses playback if PREF_PAUSE_ON_HEADSET_DISCONNECT was set to true.
+ */
+ private void pauseIfPauseOnDisconnect() {
+ if (UserPreferences.isPauseOnHeadsetDisconnect()) {
+ mediaPlayer.pause(true, true);
+ }
+ }
+
+ private BroadcastReceiver shutdownReceiver = new BroadcastReceiver() {
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (intent.getAction() != null &&
+ intent.getAction().equals(ACTION_SHUTDOWN_PLAYBACK_SERVICE)) {
+ stopSelf();
+ }
+ }
+
+ };
+
+ private BroadcastReceiver skipCurrentEpisodeReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (intent.getAction() != null &&
+ intent.getAction().equals(ACTION_SKIP_CURRENT_EPISODE)) {
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Received SKIP_CURRENT_EPISODE intent");
+ mediaPlayer.endPlayback();
+ }
+ }
+ };
+
+ public static MediaType getCurrentMediaType() {
+ return currentMediaType;
+ }
+
+ public void resume() {
+ mediaPlayer.resume();
+ }
+
+ public void prepare() {
+ mediaPlayer.prepare();
+ }
+
+ public void pause(boolean abandonAudioFocus, boolean reinit) {
+ mediaPlayer.pause(abandonAudioFocus, reinit);
+ }
+
+ public void reinit() {
+ mediaPlayer.reinit();
+ }
+
+ public PlaybackServiceMediaPlayer.PSMPInfo getPSMPInfo() {
+ return mediaPlayer.getPSMPInfo();
+ }
+
+ public PlayerStatus getStatus() {
+ return mediaPlayer.getPSMPInfo().playerStatus;
+ }
+
+ public Playable getPlayable() {
+ return mediaPlayer.getPSMPInfo().playable;
+ }
+
+ public void setSpeed(float speed) {
+ mediaPlayer.setSpeed(speed);
+ }
+
+ public boolean canSetSpeed() {
+ return mediaPlayer.canSetSpeed();
+ }
+
+ public float getCurrentPlaybackSpeed() {
+ return mediaPlayer.getPlaybackSpeed();
+ }
+
+ public boolean isStartWhenPrepared() {
+ return mediaPlayer.isStartWhenPrepared();
+ }
+
+ public void setStartWhenPrepared(boolean s) {
+ mediaPlayer.setStartWhenPrepared(s);
+ }
+
+
+ public void seekTo(final int t) {
+ mediaPlayer.seekTo(t);
+ }
+
+
+ public void seekDelta(final int d) {
+ mediaPlayer.seekDelta(d);
+ }
+
+ /**
+ * @see de.danoeh.antennapod.service.playback.PlaybackServiceMediaPlayer#seekToChapter(de.danoeh.antennapod.feed.Chapter)
+ */
+ public void seekToChapter(Chapter c) {
+ mediaPlayer.seekToChapter(c);
+ }
+
+ /**
+ * call getDuration() on mediaplayer or return INVALID_TIME if player is in
+ * an invalid state.
+ */
+ public int getDuration() {
+ return mediaPlayer.getDuration();
+ }
+
+ /**
+ * call getCurrentPosition() on mediaplayer or return INVALID_TIME if player
+ * is in an invalid state.
+ */
+ public int getCurrentPosition() {
+ return mediaPlayer.getPosition();
+ }
+
+ public boolean isStreaming() {
+ return mediaPlayer.isStreaming();
+ }
+
+ public Pair getVideoSize() {
+ return mediaPlayer.getVideoSize();
+ }
+
+ private void setCurrentlyPlayingMedia(long id) {
+ SharedPreferences.Editor editor = PreferenceManager
+ .getDefaultSharedPreferences(getApplicationContext()).edit();
+ editor.putLong(PlaybackPreferences.PREF_CURRENTLY_PLAYING_MEDIA, id);
+ editor.commit();
+ }
+}
diff --git a/src/de/danoeh/antennapod/service/playback/PlaybackServiceMediaPlayer.java b/src/de/danoeh/antennapod/service/playback/PlaybackServiceMediaPlayer.java
new file mode 100644
index 000000000..52cde2718
--- /dev/null
+++ b/src/de/danoeh/antennapod/service/playback/PlaybackServiceMediaPlayer.java
@@ -0,0 +1,917 @@
+package de.danoeh.antennapod.service.playback;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.media.AudioManager;
+import android.media.RemoteControlClient;
+import android.util.Log;
+import android.util.Pair;
+import android.view.SurfaceHolder;
+import de.danoeh.antennapod.AppConfig;
+import de.danoeh.antennapod.feed.Chapter;
+import de.danoeh.antennapod.feed.MediaType;
+import de.danoeh.antennapod.preferences.UserPreferences;
+import de.danoeh.antennapod.receiver.MediaButtonReceiver;
+import de.danoeh.antennapod.util.playback.AudioPlayer;
+import de.danoeh.antennapod.util.playback.IPlayer;
+import de.danoeh.antennapod.util.playback.Playable;
+import de.danoeh.antennapod.util.playback.VideoPlayer;
+
+import java.io.IOException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.locks.ReentrantLock;
+
+/**
+ * Manages the MediaPlayer object of the PlaybackService.
+ */
+public class PlaybackServiceMediaPlayer {
+ public static final String TAG = "PlaybackServiceMediaPlayer";
+
+ /**
+ * Return value of some PSMP methods if the method call failed.
+ */
+ public static final int INVALID_TIME = -1;
+
+ private final AudioManager audioManager;
+
+ private volatile PlayerStatus playerStatus;
+ private volatile PlayerStatus statusBeforeSeeking;
+ private volatile IPlayer mediaPlayer;
+ private volatile Playable media;
+
+ private volatile boolean stream;
+ private volatile MediaType mediaType;
+ private volatile AtomicBoolean startWhenPrepared;
+ private volatile boolean pausedBecauseOfTransientAudiofocusLoss;
+ private volatile Pair videoSize;
+
+ /**
+ * Some asynchronous calls might change the state of the MediaPlayer object. Therefore calls in other threads
+ * have to wait until these operations have finished.
+ */
+ private final ReentrantLock playerLock;
+
+ private final PSMPCallback callback;
+ private final Context context;
+
+ private final ExecutorService executor;
+
+ public PlaybackServiceMediaPlayer(Context context, PSMPCallback callback) {
+ if (context == null)
+ throw new IllegalArgumentException("context = null");
+ if (callback == null)
+ throw new IllegalArgumentException("callback = null");
+
+ this.context = context;
+ this.callback = callback;
+ this.audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
+ this.playerLock = new ReentrantLock();
+ this.startWhenPrepared = new AtomicBoolean(false);
+ executor = Executors.newSingleThreadExecutor();
+ mediaPlayer = null;
+ statusBeforeSeeking = null;
+ pausedBecauseOfTransientAudiofocusLoss = false;
+ mediaType = MediaType.UNKNOWN;
+ playerStatus = PlayerStatus.STOPPED;
+ videoSize = null;
+ }
+
+ /**
+ * Starts or prepares playback of the specified Playable object. If another Playable object is already being played, the currently playing
+ * episode will be stopped and replaced with the new Playable object. If the Playable object is already being played, the method will
+ * not do anything.
+ * Whether playback starts immediately depends on the given parameters. See below for more details.
+ *
+ * States:
+ * During execution of the method, the object will be in the INITIALIZING state. The end state depends on the given parameters.
+ *
+ * If 'prepareImmediately' is set to true, the method will go into PREPARING state and after that into PREPARED state. If
+ * 'startWhenPrepared' is set to true, the method will additionally go into PLAYING state.
+ *
+ * If an unexpected error occurs while loading the Playable's metadata or while setting the MediaPlayers data source, the object
+ * will enter the ERROR state.
+ *
+ * This method is executed on an internal executor service.
+ *
+ * @param playable The Playable object that is supposed to be played. This parameter must not be null.
+ * @param stream The type of playback. If false, the Playable object MUST provide access to a locally available file via
+ * getLocalMediaUrl. If true, the Playable object MUST provide access to a resource that can be streamed by
+ * the Android MediaPlayer via getStreamUrl.
+ * @param startWhenPrepared Sets the 'startWhenPrepared' flag. This flag determines whether playback will start immediately after the
+ * episode has been prepared for playback. Setting this flag to true does NOT mean that the episode will be prepared
+ * for playback immediately (see 'prepareImmediately' parameter for more details)
+ * @param prepareImmediately Set to true if the method should also prepare the episode for playback.
+ */
+ public void playMediaObject(final Playable playable, final boolean stream, final boolean startWhenPrepared, final boolean prepareImmediately) {
+ if (playable == null)
+ throw new IllegalArgumentException("playable = null");
+ if (AppConfig.DEBUG) Log.d(TAG, "Play media object.");
+ executor.submit(new Runnable() {
+ @Override
+ public void run() {
+ playerLock.lock();
+ try {
+ playMediaObject(playable, false, stream, startWhenPrepared, prepareImmediately);
+ } catch (RuntimeException e) {
+ throw e;
+ } finally {
+ playerLock.unlock();
+ }
+ }
+ });
+ }
+
+ /**
+ * Internal implementation of playMediaObject. This method has an additional parameter that allows the caller to force a media player reset even if
+ * the given playable parameter is the same object as the currently playing media.
+ *
+ * This method requires the playerLock and is executed on the caller's thread.
+ *
+ * @see #playMediaObject(de.danoeh.antennapod.util.playback.Playable, boolean, boolean, boolean)
+ */
+ private void playMediaObject(final Playable playable, final boolean forceReset, final boolean stream, final boolean startWhenPrepared, final boolean prepareImmediately) {
+ if (playable == null)
+ throw new IllegalArgumentException("playable = null");
+ if (!playerLock.isHeldByCurrentThread())
+ throw new IllegalStateException("method requires playerLock");
+
+
+ if (media != null) {
+ if (!forceReset && media.getIdentifier().equals(playable.getIdentifier())) {
+ // episode is already playing -> ignore method call
+ return;
+ } else {
+ // stop playback of this episode
+ if (playerStatus == PlayerStatus.PAUSED || playerStatus == PlayerStatus.PLAYING || playerStatus == PlayerStatus.PREPARED) {
+ mediaPlayer.stop();
+ }
+ setPlayerStatus(PlayerStatus.INDETERMINATE, null);
+ }
+ }
+
+ this.media = playable;
+ this.stream = stream;
+ this.mediaType = media.getMediaType();
+ this.videoSize = null;
+ createMediaPlayer();
+ PlaybackServiceMediaPlayer.this.startWhenPrepared.set(startWhenPrepared);
+ setPlayerStatus(PlayerStatus.INITIALIZING, media);
+ try {
+ media.loadMetadata();
+ if (stream) {
+ mediaPlayer.setDataSource(media.getStreamUrl());
+ } else {
+ mediaPlayer.setDataSource(media.getLocalMediaUrl());
+ }
+ setPlayerStatus(PlayerStatus.INITIALIZED, media);
+
+ if (mediaType == MediaType.VIDEO) {
+ VideoPlayer vp = (VideoPlayer) mediaPlayer;
+ // vp.setVideoScalingMode(MediaPlayer.VIDEO_SCALING_MODE_SCALE_TO_FIT);
+ }
+
+ if (prepareImmediately) {
+ setPlayerStatus(PlayerStatus.PREPARING, media);
+ mediaPlayer.prepare();
+ onPrepared(startWhenPrepared);
+ }
+
+ } catch (Playable.PlayableException e) {
+ e.printStackTrace();
+ setPlayerStatus(PlayerStatus.ERROR, null);
+ } catch (IOException e) {
+ e.printStackTrace();
+ setPlayerStatus(PlayerStatus.ERROR, null);
+ } catch (IllegalStateException e) {
+ e.printStackTrace();
+ setPlayerStatus(PlayerStatus.ERROR, null);
+ }
+ }
+
+
+ /**
+ * Resumes playback if the PSMP object is in PREPARED or PAUSED state. If the PSMP object is in an invalid state.
+ * nothing will happen.
+ *
+ * This method is executed on an internal executor service.
+ */
+ public void resume() {
+ executor.submit(new Runnable() {
+ @Override
+ public void run() {
+ playerLock.lock();
+ resumeSync();
+ playerLock.unlock();
+ }
+ });
+ }
+
+ private void resumeSync() {
+ if (playerStatus == PlayerStatus.PAUSED || playerStatus == PlayerStatus.PREPARED) {
+ int focusGained = audioManager.requestAudioFocus(
+ audioFocusChangeListener, AudioManager.STREAM_MUSIC,
+ AudioManager.AUDIOFOCUS_GAIN);
+ if (focusGained == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
+
+ setSpeed(Float.parseFloat(UserPreferences.getPlaybackSpeed()));
+ mediaPlayer.start();
+ if (playerStatus == PlayerStatus.PREPARED && media.getPosition() > 0) {
+ mediaPlayer.seekTo(media.getPosition());
+ }
+
+ setPlayerStatus(PlayerStatus.PLAYING, media);
+ pausedBecauseOfTransientAudiofocusLoss = false;
+ if (android.os.Build.VERSION.SDK_INT >= 14) {
+ RemoteControlClient remoteControlClient = callback.getRemoteControlClient();
+ if (remoteControlClient != null) {
+ audioManager
+ .registerRemoteControlClient(remoteControlClient);
+ }
+ }
+ audioManager
+ .registerMediaButtonEventReceiver(new ComponentName(context.getPackageName(),
+ MediaButtonReceiver.class.getName()));
+ media.onPlaybackStart();
+
+ } else {
+ if (AppConfig.DEBUG) Log.e(TAG, "Failed to request audio focus");
+ }
+ } else {
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Call to resume() was ignored because current state of PSMP object is " + playerStatus);
+ }
+ }
+
+
+ /**
+ * Saves the current position and pauses playback. Note that, if audiofocus
+ * is abandoned, the lockscreen controls will also disapear.
+ *
+ * This method is executed on an internal executor service.
+ *
+ * @param abandonFocus is true if the service should release audio focus
+ * @param reinit is true if service should reinit after pausing if the media
+ * file is being streamed
+ */
+ public void pause(final boolean abandonFocus, final boolean reinit) {
+ executor.submit(new Runnable() {
+ @Override
+ public void run() {
+ playerLock.lock();
+
+ if (playerStatus == PlayerStatus.PLAYING) {
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Pausing playback.");
+ mediaPlayer.pause();
+ setPlayerStatus(PlayerStatus.PAUSED, media);
+
+ if (abandonFocus) {
+ audioManager.abandonAudioFocus(audioFocusChangeListener);
+ pausedBecauseOfTransientAudiofocusLoss = false;
+ }
+ if (stream && reinit) {
+ reinit();
+ }
+ } else {
+ if (AppConfig.DEBUG) Log.d(TAG, "Ignoring call to pause: Player is in " + playerStatus + " state");
+ }
+
+ playerLock.unlock();
+ }
+ });
+ }
+
+ /**
+ * Prepared media player for playback if the service is in the INITALIZED
+ * state.
+ *
+ * This method is executed on an internal executor service.
+ */
+ public void prepare() {
+ executor.submit(new Runnable() {
+ @Override
+ public void run() {
+ playerLock.lock();
+
+ if (playerStatus == PlayerStatus.INITIALIZED) {
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Preparing media player");
+ setPlayerStatus(PlayerStatus.PREPARING, media);
+ try {
+ mediaPlayer.prepare();
+ onPrepared(startWhenPrepared.get());
+ } catch (IOException e) {
+ e.printStackTrace();
+ setPlayerStatus(PlayerStatus.ERROR, null);
+ }
+ }
+ playerLock.unlock();
+
+ }
+ });
+ }
+
+ /**
+ * Called after media player has been prepared. This method is executed on the caller's thread.
+ */
+ void onPrepared(final boolean startWhenPrepared) {
+ playerLock.lock();
+
+ if (playerStatus != PlayerStatus.PREPARING) {
+ playerLock.unlock();
+ throw new IllegalStateException("Player is not in PREPARING state");
+ }
+
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Resource prepared");
+
+ if (mediaType == MediaType.VIDEO) {
+ VideoPlayer vp = (VideoPlayer) mediaPlayer;
+ videoSize = new Pair(vp.getVideoWidth(), vp.getVideoHeight());
+ }
+
+ if (media.getPosition() > 0) {
+ mediaPlayer.seekTo(media.getPosition());
+ }
+
+ if (media.getDuration() == 0) {
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Setting duration of media");
+ media.setDuration(mediaPlayer.getDuration());
+ }
+ setPlayerStatus(PlayerStatus.PREPARED, media);
+
+ if (startWhenPrepared) {
+ resumeSync();
+ }
+
+ playerLock.unlock();
+ }
+
+ /**
+ * Resets the media player and moves it into INITIALIZED state.
+ *
+ * This method is executed on an internal executor service.
+ */
+ public void reinit() {
+ executor.submit(new Runnable() {
+ @Override
+ public void run() {
+ playerLock.lock();
+
+ if (media != null) {
+ playMediaObject(media, true, stream, startWhenPrepared.get(), false);
+ } else if (mediaPlayer != null) {
+ mediaPlayer.reset();
+ } else {
+ if (AppConfig.DEBUG) Log.d(TAG, "Call to reinit was ignored: media and mediaPlayer were null");
+ }
+ playerLock.unlock();
+ }
+ });
+ }
+
+
+ /**
+ * Seeks to the specified position. If the PSMP object is in an invalid state, this method will do nothing.
+ * Invalid time values (< 0) will be ignored.
+ *
+ * This method is executed on the caller's thread.
+ */
+ private void seekToSync(int t) {
+ if (t < 0) {
+ if (AppConfig.DEBUG) Log.d(TAG, "Received invalid value for t");
+ return;
+ }
+ playerLock.lock();
+
+ if (playerStatus == PlayerStatus.PLAYING
+ || playerStatus == PlayerStatus.PAUSED
+ || playerStatus == PlayerStatus.PREPARED) {
+ if (stream) {
+ // statusBeforeSeeking = playerStatus;
+ // setPlayerStatus(PlayerStatus.SEEKING, media);
+ }
+ mediaPlayer.seekTo(t);
+
+ } else if (playerStatus == PlayerStatus.INITIALIZED) {
+ media.setPosition(t);
+ startWhenPrepared.set(true);
+ prepare();
+ }
+ playerLock.unlock();
+ }
+
+ /**
+ * Seeks to the specified position. If the PSMP object is in an invalid state, this method will do nothing.
+ * Invalid time values (< 0) will be ignored.
+ *
+ * This method is executed on an internal executor service.
+ */
+ public void seekTo(final int t) {
+ executor.submit(new Runnable() {
+ @Override
+ public void run() {
+ seekToSync(t);
+ }
+ });
+ }
+
+ /**
+ * Seek a specific position from the current position
+ *
+ * @param d offset from current position (positive or negative)
+ */
+ public void seekDelta(final int d) {
+ executor.submit(new Runnable() {
+ @Override
+ public void run() {
+ playerLock.lock();
+ int currentPosition = getPosition();
+ if (currentPosition != INVALID_TIME) {
+ seekToSync(currentPosition + d);
+ } else {
+ Log.e(TAG, "getPosition() returned INVALID_TIME in seekDelta");
+ }
+
+ playerLock.unlock();
+ }
+ });
+ }
+
+ /**
+ * Seek to the start of the specified chapter.
+ */
+ public void seekToChapter(Chapter c) {
+ if (c == null)
+ throw new IllegalArgumentException("c = null");
+ seekTo((int) c.getStart());
+ }
+
+ /**
+ * Returns the duration of the current media object or INVALID_TIME if the duration could not be retrieved.
+ */
+ public int getDuration() {
+ if (!playerLock.tryLock()) {
+ return INVALID_TIME;
+ }
+
+ int retVal = INVALID_TIME;
+ if (playerStatus == PlayerStatus.PLAYING
+ || playerStatus == PlayerStatus.PAUSED
+ || playerStatus == PlayerStatus.PREPARED) {
+ retVal = mediaPlayer.getDuration();
+ } else if (media != null && media.getDuration() > 0) {
+ retVal = media.getDuration();
+ }
+
+ playerLock.unlock();
+ return retVal;
+ }
+
+ /**
+ * Returns the position of the current media object or INVALID_TIME if the position could not be retrieved.
+ */
+ public int getPosition() {
+ if (!playerLock.tryLock()) {
+ return INVALID_TIME;
+ }
+
+ int retVal = INVALID_TIME;
+ if (playerStatus == PlayerStatus.PLAYING
+ || playerStatus == PlayerStatus.PAUSED
+ || playerStatus == PlayerStatus.PREPARED) {
+ retVal = mediaPlayer.getCurrentPosition();
+ } else if (media != null && media.getPosition() > 0) {
+ retVal = media.getPosition();
+ }
+
+ playerLock.unlock();
+ return retVal;
+ }
+
+ public boolean isStartWhenPrepared() {
+ return startWhenPrepared.get();
+ }
+
+ public void setStartWhenPrepared(boolean startWhenPrepared) {
+ this.startWhenPrepared.set(startWhenPrepared);
+ }
+
+ /**
+ * Returns true if the playback speed can be adjusted. This method can also return false if the PSMP object's
+ * internal MediaPlayer cannot be accessed at the moment.
+ */
+ public boolean canSetSpeed() {
+ if (!playerLock.tryLock()) {
+ return false;
+ }
+ boolean retVal = false;
+ if (mediaPlayer != null && media != null && media.getMediaType() == MediaType.AUDIO) {
+ retVal = (mediaPlayer).canSetSpeed();
+ }
+
+ playerLock.unlock();
+ return retVal;
+ }
+
+ /**
+ * Sets the playback speed.
+ * This method is executed on the caller's thread.
+ */
+ private void setSpeedSync(float speed) {
+ playerLock.lock();
+ if (media != null && media.getMediaType() == MediaType.AUDIO) {
+ if (mediaPlayer.canSetSpeed()) {
+ mediaPlayer.setPlaybackSpeed((float) speed);
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Playback speed was set to " + speed);
+ callback.playbackSpeedChanged(speed);
+ }
+ }
+ playerLock.unlock();
+ }
+
+ /**
+ * Sets the playback speed.
+ * This method is executed on an internal executor service.
+ */
+ public void setSpeed(final float speed) {
+ executor.submit(new Runnable() {
+ @Override
+ public void run() {
+ setSpeedSync(speed);
+ }
+ });
+ }
+
+ /**
+ * Returns the current playback speed. If the playback speed could not be retrieved, 1 is returned.
+ */
+ public float getPlaybackSpeed() {
+ if (!playerLock.tryLock()) {
+ return 1;
+ }
+
+ int retVal = 1;
+ if (playerStatus == PlayerStatus.PLAYING
+ || playerStatus == PlayerStatus.PAUSED
+ || playerStatus == PlayerStatus.PREPARED) {
+ retVal = mediaPlayer.getCurrentPosition();
+ } else if (media != null && media.getPosition() > 0) {
+ retVal = media.getPosition();
+ }
+
+ playerLock.unlock();
+ return retVal;
+ }
+
+ public MediaType getCurrentMediaType() {
+ return mediaType;
+ }
+
+ public boolean isStreaming() {
+ return stream;
+ }
+
+
+ /**
+ * Releases internally used resources. This method should only be called when the object is not used anymore.
+ */
+ public void shutdown() {
+ executor.shutdown();
+ if (mediaPlayer != null) {
+ mediaPlayer.release();
+ }
+ }
+
+ public void setVideoSurface(final SurfaceHolder surface) {
+ executor.submit(new Runnable() {
+ @Override
+ public void run() {
+ playerLock.lock();
+ if (mediaPlayer != null) {
+ mediaPlayer.setDisplay(surface);
+ }
+ playerLock.unlock();
+ }
+ });
+ }
+
+ public void resetVideoSurface() {
+ executor.submit(new Runnable() {
+ @Override
+ public void run() {
+ playerLock.lock();
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Resetting video surface");
+ mediaPlayer.setDisplay(null);
+ reinit();
+ playerLock.unlock();
+ }
+ });
+ }
+
+ /**
+ * Return width and height of the currently playing video as a pair.
+ *
+ * @return Width and height as a Pair or null if the video size could not be determined. The method might still
+ * return an invalid non-null value if the getVideoWidth() and getVideoHeight() methods of the media player return
+ * invalid values.
+ */
+ public Pair getVideoSize() {
+ if (!playerLock.tryLock()) {
+ // use cached value if lock can't be aquired
+ return videoSize;
+ }
+ Pair res;
+ if (mediaPlayer == null || playerStatus == PlayerStatus.ERROR || mediaType != MediaType.VIDEO) {
+ res = null;
+ } else {
+ VideoPlayer vp = (VideoPlayer) mediaPlayer;
+ videoSize = new Pair(vp.getVideoWidth(), vp.getVideoHeight());
+ res = videoSize;
+ }
+ playerLock.unlock();
+ return res;
+ }
+
+ /**
+ * Returns a PSMInfo object that contains information about the current state of the PSMP object.
+ *
+ * @return The PSMPInfo object.
+ */
+ public synchronized PSMPInfo getPSMPInfo() {
+ return new PSMPInfo(playerStatus, media);
+ }
+
+ /**
+ * Sets the player status of the PSMP object. PlayerStatus and media attributes have to be set at the same time
+ * so that getPSMPInfo can't return an invalid state (e.g. status is PLAYING, but media is null).
+ *
+ * This method will notify the callback about the change of the player status (even if the new status is the same
+ * as the old one).
+ *
+ * @param newStatus The new PlayerStatus. This must not be null.
+ * @param newMedia The new playable object of the PSMP object. This can be null.
+ */
+ private synchronized void setPlayerStatus(PlayerStatus newStatus, Playable newMedia) {
+ if (newStatus == null)
+ throw new IllegalArgumentException("newStatus = null");
+ if (AppConfig.DEBUG) Log.d(TAG, "Setting player status to " + newStatus);
+
+ this.playerStatus = newStatus;
+ this.media = newMedia;
+ callback.statusChanged(new PSMPInfo(playerStatus, media));
+ }
+
+ private IPlayer createMediaPlayer() {
+ if (mediaPlayer != null) {
+ mediaPlayer.release();
+ }
+ if (media == null || media.getMediaType() == MediaType.VIDEO) {
+ mediaPlayer = new VideoPlayer();
+ } else {
+ mediaPlayer = new AudioPlayer(context);
+ }
+ mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
+ return setMediaPlayerListeners(mediaPlayer);
+ }
+
+ private final AudioManager.OnAudioFocusChangeListener audioFocusChangeListener = new AudioManager.OnAudioFocusChangeListener() {
+
+ @Override
+ public void onAudioFocusChange(final int focusChange) {
+ executor.submit(new Runnable() {
+ @Override
+ public void run() {
+ playerLock.lock();
+
+ switch (focusChange) {
+ case AudioManager.AUDIOFOCUS_LOSS:
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Lost audio focus");
+ pause(true, false);
+ callback.shouldStop();
+ break;
+ case AudioManager.AUDIOFOCUS_GAIN:
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Gained audio focus");
+ if (pausedBecauseOfTransientAudiofocusLoss) // we paused => play now
+ resume();
+ else // we ducked => raise audio level back
+ audioManager.adjustStreamVolume(AudioManager.STREAM_MUSIC,
+ AudioManager.ADJUST_RAISE, 0);
+ break;
+ case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
+ if (playerStatus == PlayerStatus.PLAYING) {
+ if (!UserPreferences.shouldPauseForFocusLoss()) {
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Lost audio focus temporarily. Ducking...");
+ audioManager.adjustStreamVolume(AudioManager.STREAM_MUSIC,
+ AudioManager.ADJUST_LOWER, 0);
+ pausedBecauseOfTransientAudiofocusLoss = false;
+ } else {
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Lost audio focus temporarily. Could duck, but won't, pausing...");
+ pause(false, false);
+ pausedBecauseOfTransientAudiofocusLoss = true;
+ }
+ }
+ break;
+ case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
+ if (playerStatus == PlayerStatus.PLAYING) {
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Lost audio focus temporarily. Pausing...");
+ pause(false, false);
+ pausedBecauseOfTransientAudiofocusLoss = true;
+ }
+ }
+
+ playerLock.unlock();
+ }
+ });
+
+ }
+ };
+
+ public void endPlayback() {
+ executor.submit(new Runnable() {
+ @Override
+ public void run() {
+ playerLock.lock();
+
+ if (playerStatus != PlayerStatus.INDETERMINATE) {
+ setPlayerStatus(PlayerStatus.INDETERMINATE, media);
+ }
+ if (mediaPlayer != null) {
+ mediaPlayer.reset();
+
+ }
+ callback.endPlayback(true);
+
+ playerLock.unlock();
+ }
+ });
+ }
+
+ /**
+ * Holds information about a PSMP object.
+ */
+ public class PSMPInfo {
+ public PlayerStatus playerStatus;
+ public Playable playable;
+
+ public PSMPInfo(PlayerStatus playerStatus, Playable playable) {
+ this.playerStatus = playerStatus;
+ this.playable = playable;
+ }
+ }
+
+ public static interface PSMPCallback {
+ public void statusChanged(PSMPInfo newInfo);
+
+ public void shouldStop();
+
+ public void playbackSpeedChanged(float s);
+
+ public void onBufferingUpdate(int percent);
+
+ public boolean onMediaPlayerInfo(int code);
+
+ public boolean onMediaPlayerError(Object inObj, int what, int extra);
+
+ public boolean endPlayback(boolean playNextEpisode);
+
+ public RemoteControlClient getRemoteControlClient();
+ }
+
+ private IPlayer setMediaPlayerListeners(IPlayer mp) {
+ if (mp != null && media != null) {
+ if (media.getMediaType() == MediaType.AUDIO) {
+ ((AudioPlayer) mp)
+ .setOnCompletionListener(audioCompletionListener);
+ ((AudioPlayer) mp)
+ .setOnSeekCompleteListener(audioSeekCompleteListener);
+ ((AudioPlayer) mp).setOnErrorListener(audioErrorListener);
+ ((AudioPlayer) mp)
+ .setOnBufferingUpdateListener(audioBufferingUpdateListener);
+ ((AudioPlayer) mp).setOnInfoListener(audioInfoListener);
+ } else {
+ ((VideoPlayer) mp)
+ .setOnCompletionListener(videoCompletionListener);
+ ((VideoPlayer) mp)
+ .setOnSeekCompleteListener(videoSeekCompleteListener);
+ ((VideoPlayer) mp).setOnErrorListener(videoErrorListener);
+ ((VideoPlayer) mp)
+ .setOnBufferingUpdateListener(videoBufferingUpdateListener);
+ ((VideoPlayer) mp).setOnInfoListener(videoInfoListener);
+ }
+ }
+ return mp;
+ }
+
+ private final com.aocate.media.MediaPlayer.OnCompletionListener audioCompletionListener = new com.aocate.media.MediaPlayer.OnCompletionListener() {
+ @Override
+ public void onCompletion(com.aocate.media.MediaPlayer mp) {
+ genericOnCompletion();
+ }
+ };
+
+ private final android.media.MediaPlayer.OnCompletionListener videoCompletionListener = new android.media.MediaPlayer.OnCompletionListener() {
+ @Override
+ public void onCompletion(android.media.MediaPlayer mp) {
+ genericOnCompletion();
+ }
+ };
+
+ private void genericOnCompletion() {
+ endPlayback();
+ }
+
+ private final com.aocate.media.MediaPlayer.OnBufferingUpdateListener audioBufferingUpdateListener = new com.aocate.media.MediaPlayer.OnBufferingUpdateListener() {
+ @Override
+ public void onBufferingUpdate(com.aocate.media.MediaPlayer mp,
+ int percent) {
+ genericOnBufferingUpdate(percent);
+ }
+ };
+
+ private final android.media.MediaPlayer.OnBufferingUpdateListener videoBufferingUpdateListener = new android.media.MediaPlayer.OnBufferingUpdateListener() {
+ @Override
+ public void onBufferingUpdate(android.media.MediaPlayer mp, int percent) {
+ genericOnBufferingUpdate(percent);
+ }
+ };
+
+ private void genericOnBufferingUpdate(int percent) {
+ callback.onBufferingUpdate(percent);
+ }
+
+ private final com.aocate.media.MediaPlayer.OnInfoListener audioInfoListener = new com.aocate.media.MediaPlayer.OnInfoListener() {
+ @Override
+ public boolean onInfo(com.aocate.media.MediaPlayer mp, int what,
+ int extra) {
+ return genericInfoListener(what);
+ }
+ };
+
+ private final android.media.MediaPlayer.OnInfoListener videoInfoListener = new android.media.MediaPlayer.OnInfoListener() {
+ @Override
+ public boolean onInfo(android.media.MediaPlayer mp, int what, int extra) {
+ return genericInfoListener(what);
+ }
+ };
+
+ private boolean genericInfoListener(int what) {
+ return callback.onMediaPlayerInfo(what);
+ }
+
+ private final com.aocate.media.MediaPlayer.OnErrorListener audioErrorListener = new com.aocate.media.MediaPlayer.OnErrorListener() {
+ @Override
+ public boolean onError(com.aocate.media.MediaPlayer mp, int what,
+ int extra) {
+ return genericOnError(mp, what, extra);
+ }
+ };
+
+ private final android.media.MediaPlayer.OnErrorListener videoErrorListener = new android.media.MediaPlayer.OnErrorListener() {
+ @Override
+ public boolean onError(android.media.MediaPlayer mp, int what, int extra) {
+ return genericOnError(mp, what, extra);
+ }
+ };
+
+ private boolean genericOnError(Object inObj, int what, int extra) {
+ return callback.onMediaPlayerError(inObj, what, extra);
+ }
+
+ private final com.aocate.media.MediaPlayer.OnSeekCompleteListener audioSeekCompleteListener = new com.aocate.media.MediaPlayer.OnSeekCompleteListener() {
+ @Override
+ public void onSeekComplete(com.aocate.media.MediaPlayer mp) {
+ genericSeekCompleteListener();
+ }
+ };
+
+ private final android.media.MediaPlayer.OnSeekCompleteListener videoSeekCompleteListener = new android.media.MediaPlayer.OnSeekCompleteListener() {
+ @Override
+ public void onSeekComplete(android.media.MediaPlayer mp) {
+ genericSeekCompleteListener();
+ }
+ };
+
+ private final void genericSeekCompleteListener() {
+ executor.submit(new Runnable() {
+ @Override
+ public void run() {
+ playerLock.lock();
+ if (playerStatus == PlayerStatus.SEEKING) {
+ setPlayerStatus(statusBeforeSeeking, media);
+ }
+ playerLock.unlock();
+ }
+ });
+ }
+}
diff --git a/src/de/danoeh/antennapod/service/playback/PlaybackServiceTaskManager.java b/src/de/danoeh/antennapod/service/playback/PlaybackServiceTaskManager.java
new file mode 100644
index 000000000..4060ab041
--- /dev/null
+++ b/src/de/danoeh/antennapod/service/playback/PlaybackServiceTaskManager.java
@@ -0,0 +1,384 @@
+package de.danoeh.antennapod.service.playback;
+
+import android.content.Context;
+import android.util.Log;
+import de.danoeh.antennapod.AppConfig;
+import de.danoeh.antennapod.feed.EventDistributor;
+import de.danoeh.antennapod.feed.FeedItem;
+import de.danoeh.antennapod.storage.DBReader;
+import de.danoeh.antennapod.util.playback.Playable;
+
+import java.util.List;
+import java.util.concurrent.*;
+
+/**
+ * Manages the background tasks of PlaybackSerivce, i.e.
+ * the sleep timer, the position saver, the widget updater and
+ * the queue loader.
+ *
+ * The PlaybackServiceTaskManager(PSTM) uses a callback object (PSTMCallback)
+ * to notify the PlaybackService about updates from the running tasks.
+ */
+public class PlaybackServiceTaskManager {
+ private static final String TAG = "PlaybackServiceTaskManager";
+
+ /**
+ * Update interval of position saver in milliseconds.
+ */
+ public static final int POSITION_SAVER_WAITING_INTERVAL = 5000;
+ /**
+ * Notification interval of widget updater in milliseconds.
+ */
+ public static final int WIDGET_UPDATER_NOTIFICATION_INTERVAL = 1500;
+
+ private static final int SCHED_EX_POOL_SIZE = 2;
+ private final ScheduledThreadPoolExecutor schedExecutor;
+
+ private ScheduledFuture positionSaverFuture;
+ private ScheduledFuture widgetUpdaterFuture;
+ private ScheduledFuture sleepTimerFuture;
+ private volatile Future> queueFuture;
+ private volatile Future chapterLoaderFuture;
+
+ private SleepTimer sleepTimer;
+
+ private final Context context;
+ private final PSTMCallback callback;
+
+ /**
+ * Sets up a new PSTM. This method will also start the queue loader task.
+ *
+ * @param context
+ * @param callback A PSTMCallback object for notifying the user about updates. Must not be null.
+ */
+ public PlaybackServiceTaskManager(Context context, PSTMCallback callback) {
+ if (context == null)
+ throw new IllegalArgumentException("context must not be null");
+ if (callback == null)
+ throw new IllegalArgumentException("callback must not be null");
+
+ this.context = context;
+ this.callback = callback;
+ schedExecutor = new ScheduledThreadPoolExecutor(SCHED_EX_POOL_SIZE, new ThreadFactory() {
+ @Override
+ public Thread newThread(Runnable r) {
+ Thread t = new Thread(r);
+ t.setPriority(Thread.MIN_PRIORITY);
+ return t;
+ }
+ });
+ loadQueue();
+ EventDistributor.getInstance().register(eventDistributorListener);
+ }
+
+ private final EventDistributor.EventListener eventDistributorListener = new EventDistributor.EventListener() {
+ @Override
+ public void update(EventDistributor eventDistributor, Integer arg) {
+ if ((EventDistributor.QUEUE_UPDATE & arg) != 0) {
+ cancelQueueLoader();
+ loadQueue();
+ }
+ }
+ };
+
+ private synchronized boolean isQueueLoaderActive() {
+ return queueFuture != null && !queueFuture.isDone();
+ }
+
+ private synchronized void cancelQueueLoader() {
+ if (isQueueLoaderActive()) {
+ queueFuture.cancel(true);
+ }
+ }
+
+ private synchronized void loadQueue() {
+ if (!isQueueLoaderActive()) {
+ queueFuture = schedExecutor.submit(new Callable>() {
+ @Override
+ public List call() throws Exception {
+ return DBReader.getQueue(context);
+ }
+ });
+ }
+ }
+
+ /**
+ * Returns the queue if it is already loaded or null if it hasn't been loaded yet.
+ * In order to wait until the queue has been loaded, use getQueue()
+ */
+ public synchronized List getQueueIfLoaded() {
+ if (queueFuture.isDone()) {
+ try {
+ return queueFuture.get();
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ } catch (ExecutionException e) {
+ e.printStackTrace();
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Returns the queue or waits until the PSTM has loaded the queue from the database.
+ */
+ public synchronized List getQueue() throws InterruptedException {
+ try {
+ return queueFuture.get();
+ } catch (ExecutionException e) {
+ throw new IllegalArgumentException(e);
+ }
+ }
+
+ /**
+ * Starts the position saver task. If the position saver is already active, nothing will happen.
+ */
+ public synchronized void startPositionSaver() {
+ if (!isPositionSaverActive()) {
+ Runnable positionSaver = new Runnable() {
+ @Override
+ public void run() {
+ callback.positionSaverTick();
+ }
+ };
+ positionSaverFuture = schedExecutor.scheduleWithFixedDelay(positionSaver, POSITION_SAVER_WAITING_INTERVAL,
+ POSITION_SAVER_WAITING_INTERVAL, TimeUnit.MILLISECONDS);
+
+ if (AppConfig.DEBUG) Log.d(TAG, "Started PositionSaver");
+ } else {
+ if (AppConfig.DEBUG) Log.d(TAG, "Call to startPositionSaver was ignored.");
+ }
+ }
+
+ /**
+ * Returns true if the position saver is currently running.
+ */
+ public synchronized boolean isPositionSaverActive() {
+ return positionSaverFuture != null && !positionSaverFuture.isCancelled() && !positionSaverFuture.isDone();
+ }
+
+ /**
+ * Cancels the position saver. If the position saver is not running, nothing will happen.
+ */
+ public synchronized void cancelPositionSaver() {
+ if (isPositionSaverActive()) {
+ positionSaverFuture.cancel(false);
+ if (AppConfig.DEBUG) Log.d(TAG, "Cancelled PositionSaver");
+ }
+ }
+
+ /**
+ * Starts the widget updater task. If the widget updater is already active, nothing will happen.
+ */
+ public synchronized void startWidgetUpdater() {
+ if (!isWidgetUpdaterActive()) {
+ Runnable widgetUpdater = new Runnable() {
+ @Override
+ public void run() {
+ callback.onWidgetUpdaterTick();
+ }
+ };
+ widgetUpdaterFuture = schedExecutor.scheduleWithFixedDelay(widgetUpdater, WIDGET_UPDATER_NOTIFICATION_INTERVAL,
+ WIDGET_UPDATER_NOTIFICATION_INTERVAL, TimeUnit.MILLISECONDS);
+
+ if (AppConfig.DEBUG) Log.d(TAG, "Started WidgetUpdater");
+ } else {
+ if (AppConfig.DEBUG) Log.d(TAG, "Call to startWidgetUpdater was ignored.");
+ }
+ }
+
+ /**
+ * Starts a new sleep timer with the given waiting time. If another sleep timer is already active, it will be
+ * cancelled first.
+ * After waitingTime has elapsed, onSleepTimerExpired() will be called.
+ *
+ * @throws java.lang.IllegalArgumentException if waitingTime <= 0
+ */
+ public synchronized void setSleepTimer(long waitingTime) {
+ if (waitingTime <= 0)
+ throw new IllegalArgumentException("waitingTime <= 0");
+
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Setting sleep timer to " + Long.toString(waitingTime)
+ + " milliseconds");
+ if (isSleepTimerActive()) {
+ sleepTimerFuture.cancel(true);
+ }
+ sleepTimer = new SleepTimer(waitingTime);
+ sleepTimerFuture = schedExecutor.schedule(sleepTimer, 0, TimeUnit.MILLISECONDS);
+ }
+
+ /**
+ * Returns true if the sleep timer is currently active.
+ */
+ public synchronized boolean isSleepTimerActive() {
+ return sleepTimer != null && sleepTimerFuture != null && !sleepTimerFuture.isCancelled() && !sleepTimerFuture.isDone();
+ }
+
+ /**
+ * Disables the sleep timer. If the sleep timer is not active, nothing will happen.
+ */
+ public synchronized void disableSleepTimer() {
+ if (isSleepTimerActive()) {
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Disabling sleep timer");
+ sleepTimerFuture.cancel(true);
+ }
+ }
+
+ /**
+ * Returns the current sleep timer time or 0 if the sleep timer is not active.
+ */
+ public synchronized long getSleepTimerTimeLeft() {
+ if (isSleepTimerActive()) {
+ return sleepTimer.getWaitingTime();
+ } else {
+ return 0;
+ }
+ }
+
+
+ /**
+ * Returns true if the widget updater is currently running.
+ */
+ public synchronized boolean isWidgetUpdaterActive() {
+ return widgetUpdaterFuture != null && !widgetUpdaterFuture.isCancelled() && !widgetUpdaterFuture.isDone();
+ }
+
+ /**
+ * Cancels the widget updater. If the widget updater is not running, nothing will happen.
+ */
+ public synchronized void cancelWidgetUpdater() {
+ if (isWidgetUpdaterActive()) {
+ widgetUpdaterFuture.cancel(false);
+ if (AppConfig.DEBUG) Log.d(TAG, "Cancelled WidgetUpdater");
+ }
+ }
+
+ private synchronized void cancelChapterLoader() {
+ if (isChapterLoaderActive()) {
+ chapterLoaderFuture.cancel(true);
+ }
+ }
+
+ private synchronized boolean isChapterLoaderActive() {
+ return chapterLoaderFuture != null && !chapterLoaderFuture.isDone();
+ }
+
+ /**
+ * Starts a new thread that loads the chapter marks from a playable object. If another chapter loader is already active,
+ * it will be cancelled first.
+ * On completion, the callback's onChapterLoaded method will be called.
+ */
+ public synchronized void startChapterLoader(final Playable media) {
+ if (media == null)
+ throw new IllegalArgumentException("media = null");
+
+ if (isChapterLoaderActive()) {
+ cancelChapterLoader();
+ }
+
+ Runnable chapterLoader = new Runnable() {
+ @Override
+ public void run() {
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Chapter loader started");
+ if (media.getChapters() == null) {
+ media.loadChapterMarks();
+ if (!Thread.currentThread().isInterrupted() && media.getChapters() != null) {
+ callback.onChapterLoaded(media);
+ }
+ }
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Chapter loader stopped");
+ }
+ };
+ chapterLoaderFuture = schedExecutor.submit(chapterLoader);
+ }
+
+
+ /**
+ * Cancels all tasks. The PSTM will be in the initial state after execution of this method.
+ */
+ public synchronized void cancelAllTasks() {
+ cancelPositionSaver();
+ cancelWidgetUpdater();
+ disableSleepTimer();
+ cancelQueueLoader();
+ cancelChapterLoader();
+ }
+
+ /**
+ * Cancels all tasks and shuts down the internal executor service of the PSTM. The object should not be used after
+ * execution of this method.
+ */
+ public synchronized void shutdown() {
+ EventDistributor.getInstance().unregister(eventDistributorListener);
+ cancelAllTasks();
+ schedExecutor.shutdown();
+ }
+
+ /**
+ * Sleeps for a given time and then pauses playback.
+ */
+ private class SleepTimer implements Runnable {
+ private static final String TAG = "SleepTimer";
+ private static final long UPDATE_INTERVALL = 1000L;
+ private volatile long waitingTime;
+ private volatile boolean isWaiting;
+
+ public SleepTimer(long waitingTime) {
+ super();
+ this.waitingTime = waitingTime;
+ isWaiting = true;
+ }
+
+ @Override
+ public void run() {
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Starting");
+ while (waitingTime > 0) {
+ try {
+ Thread.sleep(UPDATE_INTERVALL);
+ waitingTime -= UPDATE_INTERVALL;
+
+ if (waitingTime <= 0) {
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Waiting completed");
+ if (!Thread.currentThread().isInterrupted()) {
+ callback.onSleepTimerExpired();
+ }
+ postExecute();
+ }
+ } catch (InterruptedException e) {
+ Log.d(TAG, "Thread was interrupted while waiting");
+ break;
+ }
+ }
+ postExecute();
+ }
+
+ protected void postExecute() {
+ isWaiting = false;
+ }
+
+ public long getWaitingTime() {
+ return waitingTime;
+ }
+
+ public boolean isWaiting() {
+ return isWaiting;
+ }
+
+ }
+
+ public static interface PSTMCallback {
+ void positionSaverTick();
+
+ void onSleepTimerExpired();
+
+ void onWidgetUpdaterTick();
+
+ void onChapterLoaded(Playable media);
+ }
+}
diff --git a/src/de/danoeh/antennapod/service/PlayerStatus.java b/src/de/danoeh/antennapod/service/playback/PlayerStatus.java
similarity index 58%
rename from src/de/danoeh/antennapod/service/PlayerStatus.java
rename to src/de/danoeh/antennapod/service/playback/PlayerStatus.java
index fbf5b1505..3d2b4ad39 100644
--- a/src/de/danoeh/antennapod/service/PlayerStatus.java
+++ b/src/de/danoeh/antennapod/service/playback/PlayerStatus.java
@@ -1,14 +1,14 @@
-package de.danoeh.antennapod.service;
+package de.danoeh.antennapod.service.playback;
public enum PlayerStatus {
+ INDETERMINATE, // player is currently changing its state, listeners should wait until the player has left this state.
ERROR,
PREPARING,
PAUSED,
PLAYING,
STOPPED,
PREPARED,
- SEEKING,
- AWAITING_VIDEO_SURFACE, // player has been initialized and the media type to be played is a video.
+ SEEKING,
INITIALIZING, // playback service is loading the Playable's metadata
INITIALIZED // playback service was started, data source of media player was set.
}
diff --git a/src/de/danoeh/antennapod/service/PlayerWidgetService.java b/src/de/danoeh/antennapod/service/playback/PlayerWidgetService.java
similarity index 92%
rename from src/de/danoeh/antennapod/service/PlayerWidgetService.java
rename to src/de/danoeh/antennapod/service/playback/PlayerWidgetService.java
index 475af9655..f4674d395 100644
--- a/src/de/danoeh/antennapod/service/PlayerWidgetService.java
+++ b/src/de/danoeh/antennapod/service/playback/PlayerWidgetService.java
@@ -1,4 +1,4 @@
-package de.danoeh.antennapod.service;
+package de.danoeh.antennapod.service.playback;
import android.app.PendingIntent;
import android.app.Service;
@@ -72,9 +72,11 @@ public class PlayerWidgetService extends Service {
}
private void updateViews() {
+ if (playbackService == null) {
+ return;
+ }
isUpdating = true;
- if (AppConfig.DEBUG)
- Log.d(TAG, "Updating widget views");
+
ComponentName playerWidget = new ComponentName(this, PlayerWidget.class);
AppWidgetManager manager = AppWidgetManager.getInstance(this);
RemoteViews views = new RemoteViews(getPackageName(),
@@ -83,8 +85,8 @@ public class PlayerWidgetService extends Service {
PlaybackService.getPlayerActivityIntent(this), 0);
views.setOnClickPendingIntent(R.id.layout_left, startMediaplayer);
- if (playbackService != null && playbackService.getMedia() != null) {
- Playable media = playbackService.getMedia();
+ final Playable media = playbackService.getPlayable();
+ if (playbackService != null && media != null) {
PlayerStatus status = playbackService.getStatus();
views.setTextViewText(R.id.txtvTitle, media.getEpisodeTitle());
@@ -101,8 +103,6 @@ public class PlayerWidgetService extends Service {
views.setOnClickPendingIntent(R.id.butPlay,
createMediaButtonIntent());
} else {
- if (AppConfig.DEBUG)
- Log.d(TAG, "No media playing. Displaying defaultt views");
views.setViewVisibility(R.id.txtvProgress, View.INVISIBLE);
views.setTextViewText(R.id.txtvTitle,
this.getString(R.string.no_media_playing_label));
@@ -126,8 +126,8 @@ public class PlayerWidgetService extends Service {
}
private String getProgressString(PlaybackService ps) {
- int position = ps.getCurrentPositionSafe();
- int duration = ps.getDurationSafe();
+ int position = ps.getCurrentPosition();
+ int duration = ps.getDuration();
if (position != PlaybackService.INVALID_TIME
&& duration != PlaybackService.INVALID_TIME) {
return Converter.getDurationStringLong(position) + " / "
diff --git a/src/de/danoeh/antennapod/storage/DBTasks.java b/src/de/danoeh/antennapod/storage/DBTasks.java
index 26d5c712a..3b6ab99ce 100644
--- a/src/de/danoeh/antennapod/storage/DBTasks.java
+++ b/src/de/danoeh/antennapod/storage/DBTasks.java
@@ -8,7 +8,7 @@ import de.danoeh.antennapod.AppConfig;
import de.danoeh.antennapod.feed.*;
import de.danoeh.antennapod.preferences.UserPreferences;
import de.danoeh.antennapod.service.GpodnetSyncService;
-import de.danoeh.antennapod.service.PlaybackService;
+import de.danoeh.antennapod.service.playback.PlaybackService;
import de.danoeh.antennapod.service.download.DownloadStatus;
import de.danoeh.antennapod.util.DownloadError;
import de.danoeh.antennapod.util.NetworkUtils;
diff --git a/src/de/danoeh/antennapod/storage/DBWriter.java b/src/de/danoeh/antennapod/storage/DBWriter.java
index 6be1a5327..db498efb5 100644
--- a/src/de/danoeh/antennapod/storage/DBWriter.java
+++ b/src/de/danoeh/antennapod/storage/DBWriter.java
@@ -20,8 +20,7 @@ import de.danoeh.antennapod.AppConfig;
import de.danoeh.antennapod.feed.*;
import de.danoeh.antennapod.preferences.GpodnetPreferences;
import de.danoeh.antennapod.preferences.PlaybackPreferences;
-import de.danoeh.antennapod.service.GpodnetSyncService;
-import de.danoeh.antennapod.service.PlaybackService;
+import de.danoeh.antennapod.service.playback.PlaybackService;
import de.danoeh.antennapod.service.download.DownloadStatus;
import de.danoeh.antennapod.util.QueueAccess;
diff --git a/src/de/danoeh/antennapod/util/menuhandler/FeedItemMenuHandler.java b/src/de/danoeh/antennapod/util/menuhandler/FeedItemMenuHandler.java
index e99a733dc..38d990b4b 100644
--- a/src/de/danoeh/antennapod/util/menuhandler/FeedItemMenuHandler.java
+++ b/src/de/danoeh/antennapod/util/menuhandler/FeedItemMenuHandler.java
@@ -7,7 +7,7 @@ import de.danoeh.antennapod.AppConfig;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.asynctask.FlattrClickWorker;
import de.danoeh.antennapod.feed.FeedItem;
-import de.danoeh.antennapod.service.PlaybackService;
+import de.danoeh.antennapod.service.playback.PlaybackService;
import de.danoeh.antennapod.storage.DBTasks;
import de.danoeh.antennapod.storage.DBWriter;
import de.danoeh.antennapod.storage.DownloadRequestException;
@@ -15,8 +15,6 @@ import de.danoeh.antennapod.storage.DownloadRequester;
import de.danoeh.antennapod.util.QueueAccess;
import de.danoeh.antennapod.util.ShareUtils;
-import java.util.List;
-
/** Handles interactions with the FeedItemMenu. */
public class FeedItemMenuHandler {
private FeedItemMenuHandler() {
diff --git a/src/de/danoeh/antennapod/util/playback/AudioPlayer.java b/src/de/danoeh/antennapod/util/playback/AudioPlayer.java
index 68d31324d..0945303e4 100644
--- a/src/de/danoeh/antennapod/util/playback/AudioPlayer.java
+++ b/src/de/danoeh/antennapod/util/playback/AudioPlayer.java
@@ -27,4 +27,9 @@ public class AudioPlayer extends MediaPlayer implements IPlayer {
throw new UnsupportedOperationException("Setting display not supported in Audio Player");
}
}
+
+ @Override
+ public void setVideoScalingMode(int mode) {
+ throw new UnsupportedOperationException("Setting scaling mode is not supported in Audio Player");
+ }
}
diff --git a/src/de/danoeh/antennapod/util/playback/IPlayer.java b/src/de/danoeh/antennapod/util/playback/IPlayer.java
index ca9b36358..8c1cf4ef4 100644
--- a/src/de/danoeh/antennapod/util/playback/IPlayer.java
+++ b/src/de/danoeh/antennapod/util/playback/IPlayer.java
@@ -61,4 +61,6 @@ public interface IPlayer {
void start();
void stop();
+
+ public void setVideoScalingMode(int mode);
}
diff --git a/src/de/danoeh/antennapod/util/playback/PlaybackController.java b/src/de/danoeh/antennapod/util/playback/PlaybackController.java
index 017a0cd5b..2b83bcac2 100644
--- a/src/de/danoeh/antennapod/util/playback/PlaybackController.java
+++ b/src/de/danoeh/antennapod/util/playback/PlaybackController.java
@@ -1,25 +1,14 @@
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.*;
import android.content.res.TypedArray;
+import android.media.MediaPlayer;
import android.os.AsyncTask;
import android.os.IBinder;
import android.preference.PreferenceManager;
import android.util.Log;
+import android.util.Pair;
import android.view.SurfaceHolder;
import android.view.View;
import android.view.View.OnClickListener;
@@ -30,13 +19,17 @@ import de.danoeh.antennapod.AppConfig;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.feed.Chapter;
import de.danoeh.antennapod.feed.FeedMedia;
+import de.danoeh.antennapod.feed.MediaType;
import de.danoeh.antennapod.preferences.PlaybackPreferences;
-import de.danoeh.antennapod.service.PlaybackService;
-import de.danoeh.antennapod.service.PlayerStatus;
+import de.danoeh.antennapod.service.playback.PlaybackService;
+import de.danoeh.antennapod.service.playback.PlaybackServiceMediaPlayer;
+import de.danoeh.antennapod.service.playback.PlayerStatus;
import de.danoeh.antennapod.storage.DBTasks;
import de.danoeh.antennapod.util.Converter;
import de.danoeh.antennapod.util.playback.Playable.PlayableUtils;
+import java.util.concurrent.*;
+
/**
* Communicates with the playback service. GUI classes should use this class to
* control playback instead of communicating with the PlaybackService directly.
@@ -44,10 +37,10 @@ import de.danoeh.antennapod.util.playback.Playable.PlayableUtils;
public abstract class PlaybackController {
private static final String TAG = "PlaybackController";
- public static final int DEFAULT_SEEK_DELTA = 30000;
- public static final int INVALID_TIME = -1;
+ public static final int DEFAULT_SEEK_DELTA = 30000;
+ public static final int INVALID_TIME = -1;
- private Activity activity;
+ private final Activity activity;
private PlaybackService playbackService;
private Playable media;
@@ -69,6 +62,8 @@ public abstract class PlaybackController {
private boolean reinitOnPause;
public PlaybackController(Activity activity, boolean reinitOnPause) {
+ if (activity == null)
+ throw new IllegalArgumentException("activity = null");
this.activity = activity;
this.reinitOnPause = reinitOnPause;
schedExecutor = new ScheduledThreadPoolExecutor(SCHED_EX_POOLSIZE,
@@ -157,9 +152,6 @@ public abstract class PlaybackController {
*/
public void pause() {
mediaInfoLoaded = false;
- if (playbackService != null && playbackService.isPlayingVideo()) {
- playbackService.pause(true, true);
- }
}
/**
@@ -181,6 +173,7 @@ public abstract class PlaybackController {
boolean bound = false;
if (!PlaybackService.isRunning) {
if (serviceIntent != null) {
+ if (AppConfig.DEBUG) Log.d(TAG, "Calling start service");
activity.startService(serviceIntent);
bound = activity.bindService(serviceIntent, mConnection, 0);
} else {
@@ -297,7 +290,9 @@ public abstract class PlaybackController {
if (AppConfig.DEBUG)
Log.d(TAG, "Received statusUpdate Intent.");
if (isConnectedToPlaybackService()) {
- status = playbackService.getStatus();
+ PlaybackServiceMediaPlayer.PSMPInfo info = playbackService.getPSMPInfo();
+ status = info.playerStatus;
+ media = info.playable;
handleStatus();
} else {
Log.w(TAG,
@@ -328,10 +323,9 @@ public abstract class PlaybackController {
case PlaybackService.NOTIFICATION_TYPE_RELOAD:
cancelPositionObserver();
mediaInfoLoaded = false;
+ queryService();
onReloadNotification(intent.getIntExtra(
PlaybackService.EXTRA_NOTIFICATION_CODE, -1));
- queryService();
-
break;
case PlaybackService.NOTIFICATION_TYPE_SLEEPTIMER_UPDATE:
onSleepTimerUpdate();
@@ -401,16 +395,24 @@ public abstract class PlaybackController {
* 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();
+ final int playResource;
+ final int pauseResource;
+ if (PlaybackService.getCurrentMediaType() == MediaType.AUDIO) {
+ TypedArray res = activity.obtainStyledAttributes(new int[]{
+ R.attr.av_play, R.attr.av_pause});
+ playResource = res.getResourceId(0, R.drawable.av_play);
+ pauseResource = res.getResourceId(1, R.drawable.av_pause);
+ res.recycle();
+ } else {
+ playResource = R.drawable.ic_action_play_over_video;
+ pauseResource = R.drawable.ic_action_pause_over_video;
+ }
switch (status) {
case ERROR:
postStatusMsg(R.string.player_error_msg);
+ handleError(MediaPlayer.MEDIA_ERROR_UNKNOWN);
break;
case PAUSED:
clearStatusMsg();
@@ -421,6 +423,9 @@ public abstract class PlaybackController {
case PLAYING:
clearStatusMsg();
checkMediaInfoLoaded();
+ if (PlaybackService.getCurrentMediaType() == MediaType.VIDEO) {
+ onAwaitingVideoSurface();
+ }
setupPositionObserver();
updatePlayButtonAppearance(pauseResource);
break;
@@ -446,9 +451,6 @@ public abstract class PlaybackController {
case SEEKING:
postStatusMsg(R.string.player_seeking_msg);
break;
- case AWAITING_VIDEO_SURFACE:
- onAwaitingVideoSurface();
- break;
case INITIALIZED:
checkMediaInfoLoaded();
clearStatusMsg();
@@ -458,10 +460,7 @@ public abstract class PlaybackController {
}
private void checkMediaInfoLoaded() {
- if (!mediaInfoLoaded) {
- loadMediaInfo();
- }
- mediaInfoLoaded = true;
+ mediaInfoLoaded = (mediaInfoLoaded || loadMediaInfo());
}
private void updatePlayButtonAppearance(int resource) {
@@ -475,7 +474,7 @@ public abstract class PlaybackController {
public abstract void clearStatusMsg();
- public abstract void loadMediaInfo();
+ public abstract boolean loadMediaInfo();
public abstract void onAwaitingVideoSurface();
@@ -488,7 +487,8 @@ public abstract class PlaybackController {
Log.d(TAG, "Querying service info");
if (playbackService != null) {
status = playbackService.getStatus();
- media = playbackService.getMedia();
+ media = playbackService.getPlayable();
+ /*
if (media == null) {
Log.w(TAG,
"PlaybackService has no media object. Trying to restore last played media.");
@@ -497,6 +497,7 @@ public abstract class PlaybackController {
activity.startService(serviceIntent);
}
}
+ */
onServiceQueried();
setupGUI();
@@ -517,7 +518,7 @@ public abstract class PlaybackController {
*/
public float onSeekBarProgressChanged(SeekBar seekBar, int progress,
boolean fromUser, TextView txtvPosition) {
- if (fromUser && playbackService != null) {
+ if (fromUser && playbackService != null && media != null) {
float prog = progress / ((float) seekBar.getMax());
int duration = media.getDuration();
txtvPosition.setText(Converter
@@ -541,7 +542,7 @@ public abstract class PlaybackController {
*/
public void onSeekBarStopTrackingTouch(SeekBar seekBar, float prog) {
if (playbackService != null) {
- playbackService.seek((int) (prog * media.getDuration()));
+ playbackService.seekTo((int) (prog * media.getDuration()));
setupPositionObserver();
}
}
@@ -557,7 +558,7 @@ public abstract class PlaybackController {
break;
case PAUSED:
case PREPARED:
- playbackService.play();
+ playbackService.resume();
break;
case PREPARING:
playbackService.setStartWhenPrepared(!playbackService
@@ -609,7 +610,7 @@ public abstract class PlaybackController {
public int getPosition() {
if (playbackService != null) {
- return playbackService.getCurrentPositionSafe();
+ return playbackService.getCurrentPosition();
} else {
return PlaybackService.INVALID_TIME;
}
@@ -617,7 +618,7 @@ public abstract class PlaybackController {
public int getDuration() {
if (playbackService != null) {
- return playbackService.getDurationSafe();
+ return playbackService.getDuration();
} else {
return PlaybackService.INVALID_TIME;
}
@@ -675,27 +676,35 @@ public abstract class PlaybackController {
return playbackService != null && playbackService.canSetSpeed();
}
- public void setPlaybackSpeed(float speed) {
- if (playbackService != null) {
- playbackService.setSpeed(speed);
- }
- }
-
- public float getCurrentPlaybackSpeedMultiplier() {
- if (canSetPlaybackSpeed()) {
- return playbackService.getCurrentPlaybackSpeed();
- } else {
- return -1;
- }
- }
+ public void setPlaybackSpeed(float speed) {
+ if (playbackService != null) {
+ playbackService.setSpeed(speed);
+ }
+ }
+
+ public float getCurrentPlaybackSpeedMultiplier() {
+ if (canSetPlaybackSpeed()) {
+ return playbackService.getCurrentPlaybackSpeed();
+ } else {
+ return -1;
+ }
+ }
public boolean isPlayingVideo() {
if (playbackService != null) {
- return PlaybackService.isPlayingVideo();
+ return PlaybackService.getCurrentMediaType() == MediaType.VIDEO;
}
return false;
}
+ public Pair getVideoSize() {
+ if (playbackService != null) {
+ return playbackService.getVideoSize();
+ } else {
+ return null;
+ }
+ }
+
/**
* Returns true if PlaybackController can communicate with the playback
@@ -716,7 +725,7 @@ public abstract class PlaybackController {
*/
public void reinitServiceIfPaused() {
if (playbackService != null
- && playbackService.isShouldStream()
+ && playbackService.isStreaming()
&& (playbackService.getStatus() == PlayerStatus.PAUSED || (playbackService
.getStatus() == PlayerStatus.PREPARING && playbackService
.isStartWhenPrepared() == false))) {
@@ -733,8 +742,7 @@ public abstract class PlaybackController {
@Override
public void run() {
- if (playbackService != null && playbackService.getPlayer() != null
- && playbackService.getPlayer().isPlaying()) {
+ if (playbackService != null && playbackService.getStatus() == PlayerStatus.PLAYING) {
activity.runOnUiThread(new Runnable() {
@Override
diff --git a/src/de/danoeh/antennapod/util/playback/VideoPlayer.java b/src/de/danoeh/antennapod/util/playback/VideoPlayer.java
index f0a50542c..ea9c692ab 100644
--- a/src/de/danoeh/antennapod/util/playback/VideoPlayer.java
+++ b/src/de/danoeh/antennapod/util/playback/VideoPlayer.java
@@ -59,4 +59,9 @@ public class VideoPlayer extends MediaPlayer implements IPlayer {
Log.e(TAG, "Setting playback speed unsupported in video player");
throw new UnsupportedOperationException("Setting playback speed unsupported in video player");
}
+
+ @Override
+ public void setVideoScalingMode(int mode) {
+ super.setVideoScalingMode(mode);
+ }
}
diff --git a/src/de/danoeh/antennapod/view/AspectRatioVideoView.java b/src/de/danoeh/antennapod/view/AspectRatioVideoView.java
new file mode 100644
index 000000000..f930c912a
--- /dev/null
+++ b/src/de/danoeh/antennapod/view/AspectRatioVideoView.java
@@ -0,0 +1,97 @@
+package de.danoeh.antennapod.view;
+
+/*
+ * Copyright (C) Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.widget.VideoView;
+
+public class AspectRatioVideoView extends VideoView {
+
+
+ private int mVideoWidth;
+ private int mVideoHeight;
+
+ public AspectRatioVideoView(Context context) {
+ this(context, null);
+ }
+
+ public AspectRatioVideoView(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public AspectRatioVideoView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+
+ mVideoWidth = 0;
+ mVideoHeight = 0;
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ if (mVideoWidth <= 0 || mVideoHeight <= 0) {
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ return;
+ }
+
+ float heightRatio = (float) mVideoHeight / (float) getHeight();
+ float widthRatio = (float) mVideoWidth / (float) getWidth();
+
+ int scaledHeight;
+ int scaledWidth;
+
+ if (heightRatio > widthRatio) {
+ scaledHeight = (int) Math.ceil((float) mVideoHeight
+ / heightRatio);
+ scaledWidth = (int) Math.ceil((float) mVideoWidth
+ / heightRatio);
+ } else {
+ scaledHeight = (int) Math.ceil((float) mVideoHeight
+ / widthRatio);
+ scaledWidth = (int) Math.ceil((float) mVideoWidth
+ / widthRatio);
+ }
+
+ setMeasuredDimension(scaledWidth, scaledHeight);
+ }
+
+ /**
+ * Source code originally from:
+ * http://clseto.mysinablog.com/index.php?op=ViewArticle&articleId=2992625
+ *
+ * @param videoWidth
+ * @param videoHeight
+ */
+ public void setVideoSize(int videoWidth, int videoHeight) {
+ // Set the new video size
+ mVideoWidth = videoWidth;
+ mVideoHeight = videoHeight;
+
+ /**
+ * If this isn't set the video is stretched across the
+ * SurfaceHolders display surface (i.e. the SurfaceHolder
+ * as the same size and the video is drawn to fit this
+ * display area). We want the size to be the video size
+ * and allow the aspectratio to handle how the surface is shown
+ */
+ getHolder().setFixedSize(videoWidth, videoHeight);
+
+ requestLayout();
+ invalidate();
+ }
+
+}
diff --git a/src/instrumentationTest/de/test/antennapod/service/playback/PlaybackServiceMediaPlayerTest.java b/src/instrumentationTest/de/test/antennapod/service/playback/PlaybackServiceMediaPlayerTest.java
new file mode 100644
index 000000000..fac293693
--- /dev/null
+++ b/src/instrumentationTest/de/test/antennapod/service/playback/PlaybackServiceMediaPlayerTest.java
@@ -0,0 +1,1169 @@
+package instrumentationTest.de.test.antennapod.service.playback;
+
+import android.content.Context;
+import android.media.RemoteControlClient;
+import android.test.InstrumentationTestCase;
+import de.danoeh.antennapod.feed.Feed;
+import de.danoeh.antennapod.feed.FeedItem;
+import de.danoeh.antennapod.feed.FeedMedia;
+import de.danoeh.antennapod.service.playback.PlaybackServiceMediaPlayer;
+import de.danoeh.antennapod.service.playback.PlayerStatus;
+import de.danoeh.antennapod.storage.PodDBAdapter;
+import de.danoeh.antennapod.util.playback.Playable;
+import junit.framework.AssertionFailedError;
+import org.apache.commons.io.IOUtils;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Test class for PlaybackServiceMediaPlayer
+ */
+public class PlaybackServiceMediaPlayerTest extends InstrumentationTestCase {
+ private static final String TAG = "PlaybackServiceMediaPlayerTest";
+
+ private static final String PLAYABLE_FILE_URL = "http://hpr.dogphilosophy.net/test/mp3.mp3";
+ private static final String PLAYABLE_DEST_URL = "psmptestfile.wav";
+ private String PLAYABLE_LOCAL_URL = null;
+ private static final int LATCH_TIMEOUT_SECONDS = 10;
+
+ private volatile AssertionFailedError assertionError;
+
+ @Override
+ protected void tearDown() throws Exception {
+ super.tearDown();
+ PodDBAdapter.deleteDatabase(getInstrumentation().getTargetContext());
+ }
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ assertionError = null;
+
+ final Context context = getInstrumentation().getTargetContext();
+ context.deleteDatabase(PodDBAdapter.DATABASE_NAME);
+ // make sure database is created
+ PodDBAdapter adapter = new PodDBAdapter(context);
+ adapter.open();
+ adapter.close();
+ File cacheDir = context.getExternalFilesDir("testFiles");
+ if (cacheDir == null)
+ cacheDir = context.getExternalFilesDir("testFiles");
+ File dest = new File(cacheDir, PLAYABLE_DEST_URL);
+
+ assertNotNull(cacheDir);
+ assertTrue(cacheDir.canWrite());
+ assertTrue(cacheDir.canRead());
+ if (!dest.exists()) {
+ InputStream i = new URL(PLAYABLE_FILE_URL).openStream();
+ OutputStream o = new FileOutputStream(new File(cacheDir, PLAYABLE_DEST_URL));
+ IOUtils.copy(i, o);
+ o.flush();
+ o.close();
+ i.close();
+ }
+ PLAYABLE_LOCAL_URL = "file://" + dest.getAbsolutePath();
+ }
+
+ private void checkPSMPInfo(PlaybackServiceMediaPlayer.PSMPInfo info) {
+ try {
+ switch (info.playerStatus) {
+ case PLAYING:
+ case PAUSED:
+ case PREPARED:
+ case PREPARING:
+ case INITIALIZED:
+ case INITIALIZING:
+ case SEEKING:
+ assertNotNull(info.playable);
+ break;
+ case STOPPED:
+ assertNull(info.playable);
+ break;
+ case ERROR:
+ assertNull(info.playable);
+ }
+ } catch (AssertionFailedError e) {
+ if (assertionError == null)
+ assertionError = e;
+ }
+ }
+
+ public void testInit() {
+ final Context c = getInstrumentation().getTargetContext();
+ PlaybackServiceMediaPlayer psmp = new PlaybackServiceMediaPlayer(c, defaultCallback);
+ psmp.shutdown();
+ }
+
+ private Playable writeTestPlayable(String downloadUrl, String fileUrl) {
+ final Context c = getInstrumentation().getTargetContext();
+ Feed f = new Feed(0, new Date(), "f", "l", "d", null, null, null, null, "i", null, null, "l", false);
+ f.setItems(new ArrayList());
+ FeedItem i = new FeedItem(0, "t", "i", "l", new Date(), false, f);
+ f.getItems().add(i);
+ FeedMedia media = new FeedMedia(0, i, 0, 0, 0, "audio/wav", fileUrl, downloadUrl, fileUrl != null, null);
+ i.setMedia(media);
+ PodDBAdapter adapter = new PodDBAdapter(c);
+ adapter.open();
+ adapter.setCompleteFeed(f);
+ assertTrue(media.getId() != 0);
+ adapter.close();
+ return media;
+ }
+
+
+ public void testPlayMediaObjectStreamNoStartNoPrepare() throws InterruptedException {
+ final Context c = getInstrumentation().getTargetContext();
+ final CountDownLatch countDownLatch = new CountDownLatch(2);
+ PlaybackServiceMediaPlayer.PSMPCallback callback = new PlaybackServiceMediaPlayer.PSMPCallback() {
+ @Override
+ public void statusChanged(PlaybackServiceMediaPlayer.PSMPInfo newInfo) {
+ try {
+ checkPSMPInfo(newInfo);
+ if (newInfo.playerStatus == PlayerStatus.ERROR)
+ throw new IllegalStateException("MediaPlayer error");
+ if (countDownLatch.getCount() == 0) {
+ fail();
+ } else if (countDownLatch.getCount() == 2) {
+ assertEquals(PlayerStatus.INITIALIZING, newInfo.playerStatus);
+ countDownLatch.countDown();
+ } else {
+ assertEquals(PlayerStatus.INITIALIZED, newInfo.playerStatus);
+ countDownLatch.countDown();
+ }
+ } catch (AssertionFailedError e) {
+ if (assertionError == null)
+ assertionError = e;
+ }
+ }
+
+ @Override
+ public void shouldStop() {
+
+ }
+
+ @Override
+ public void playbackSpeedChanged(float s) {
+
+ }
+
+ @Override
+ public void onBufferingUpdate(int percent) {
+
+ }
+
+ @Override
+ public boolean onMediaPlayerInfo(int code) {
+ return false;
+ }
+
+ @Override
+ public boolean onMediaPlayerError(Object inObj, int what, int extra) {
+ return false;
+ }
+
+ @Override
+ public boolean endPlayback(boolean playNextEpisode) {
+ return false;
+ }
+
+ @Override
+ public RemoteControlClient getRemoteControlClient() {
+ return null;
+ }
+ };
+ PlaybackServiceMediaPlayer psmp = new PlaybackServiceMediaPlayer(c, callback);
+ Playable p = writeTestPlayable(PLAYABLE_FILE_URL, null);
+ psmp.playMediaObject(p, true, false, false);
+ boolean res = countDownLatch.await(LATCH_TIMEOUT_SECONDS, TimeUnit.SECONDS);
+ if (assertionError != null)
+ throw assertionError;
+ assertTrue(res);
+
+ assertTrue(psmp.getPSMPInfo().playerStatus == PlayerStatus.INITIALIZED);
+ assertFalse(psmp.isStartWhenPrepared());
+ psmp.shutdown();
+ }
+
+ public void testPlayMediaObjectStreamStartNoPrepare() throws InterruptedException {
+ final Context c = getInstrumentation().getTargetContext();
+ final CountDownLatch countDownLatch = new CountDownLatch(2);
+ PlaybackServiceMediaPlayer.PSMPCallback callback = new PlaybackServiceMediaPlayer.PSMPCallback() {
+ @Override
+ public void statusChanged(PlaybackServiceMediaPlayer.PSMPInfo newInfo) {
+ try {
+ checkPSMPInfo(newInfo);
+ if (newInfo.playerStatus == PlayerStatus.ERROR)
+ throw new IllegalStateException("MediaPlayer error");
+ if (countDownLatch.getCount() == 0) {
+ fail();
+ } else if (countDownLatch.getCount() == 2) {
+ assertEquals(PlayerStatus.INITIALIZING, newInfo.playerStatus);
+ countDownLatch.countDown();
+ } else {
+ assertEquals(PlayerStatus.INITIALIZED, newInfo.playerStatus);
+ countDownLatch.countDown();
+ }
+ } catch (AssertionFailedError e) {
+ if (assertionError == null)
+ assertionError = e;
+ }
+ }
+
+ @Override
+ public void shouldStop() {
+
+ }
+
+ @Override
+ public void playbackSpeedChanged(float s) {
+
+ }
+
+ @Override
+ public void onBufferingUpdate(int percent) {
+
+ }
+
+ @Override
+ public boolean onMediaPlayerInfo(int code) {
+ return false;
+ }
+
+ @Override
+ public boolean onMediaPlayerError(Object inObj, int what, int extra) {
+ return false;
+ }
+
+ @Override
+ public boolean endPlayback(boolean playNextEpisode) {
+ return false;
+ }
+
+ @Override
+ public RemoteControlClient getRemoteControlClient() {
+ return null;
+ }
+ };
+ PlaybackServiceMediaPlayer psmp = new PlaybackServiceMediaPlayer(c, callback);
+ Playable p = writeTestPlayable(PLAYABLE_FILE_URL, null);
+ psmp.playMediaObject(p, true, true, false);
+
+ boolean res = countDownLatch.await(LATCH_TIMEOUT_SECONDS, TimeUnit.SECONDS);
+ if (assertionError != null)
+ throw assertionError;
+ assertTrue(res);
+
+ assertTrue(psmp.getPSMPInfo().playerStatus == PlayerStatus.INITIALIZED);
+ assertTrue(psmp.isStartWhenPrepared());
+ psmp.shutdown();
+ }
+
+ public void testPlayMediaObjectStreamNoStartPrepare() throws InterruptedException {
+ final Context c = getInstrumentation().getTargetContext();
+ final CountDownLatch countDownLatch = new CountDownLatch(4);
+ PlaybackServiceMediaPlayer.PSMPCallback callback = new PlaybackServiceMediaPlayer.PSMPCallback() {
+ @Override
+ public void statusChanged(PlaybackServiceMediaPlayer.PSMPInfo newInfo) {
+ try {
+ checkPSMPInfo(newInfo);
+ if (newInfo.playerStatus == PlayerStatus.ERROR)
+ throw new IllegalStateException("MediaPlayer error");
+ if (countDownLatch.getCount() == 0) {
+ fail();
+ } else if (countDownLatch.getCount() == 4) {
+ assertEquals(PlayerStatus.INITIALIZING, newInfo.playerStatus);
+ } else if (countDownLatch.getCount() == 3) {
+ assertEquals(PlayerStatus.INITIALIZED, newInfo.playerStatus);
+ } else if (countDownLatch.getCount() == 2) {
+ assertEquals(PlayerStatus.PREPARING, newInfo.playerStatus);
+ } else if (countDownLatch.getCount() == 1) {
+ assertEquals(PlayerStatus.PREPARED, newInfo.playerStatus);
+ }
+ countDownLatch.countDown();
+ } catch (AssertionFailedError e) {
+ if (assertionError == null)
+ assertionError = e;
+ }
+ }
+
+ @Override
+ public void shouldStop() {
+
+ }
+
+ @Override
+ public void playbackSpeedChanged(float s) {
+
+ }
+
+ @Override
+ public void onBufferingUpdate(int percent) {
+
+ }
+
+ @Override
+ public boolean onMediaPlayerInfo(int code) {
+ return false;
+ }
+
+ @Override
+ public boolean onMediaPlayerError(Object inObj, int what, int extra) {
+ return false;
+ }
+
+ @Override
+ public boolean endPlayback(boolean playNextEpisode) {
+ return false;
+ }
+
+ @Override
+ public RemoteControlClient getRemoteControlClient() {
+ return null;
+ }
+ };
+ PlaybackServiceMediaPlayer psmp = new PlaybackServiceMediaPlayer(c, callback);
+ Playable p = writeTestPlayable(PLAYABLE_FILE_URL, null);
+ psmp.playMediaObject(p, true, false, true);
+ boolean res = countDownLatch.await(LATCH_TIMEOUT_SECONDS, TimeUnit.SECONDS);
+ if (assertionError != null)
+ throw assertionError;
+ assertTrue(res);
+ assertTrue(psmp.getPSMPInfo().playerStatus == PlayerStatus.PREPARED);
+
+ psmp.shutdown();
+ }
+
+ public void testPlayMediaObjectStreamStartPrepare() throws InterruptedException {
+ final Context c = getInstrumentation().getTargetContext();
+ final CountDownLatch countDownLatch = new CountDownLatch(5);
+ PlaybackServiceMediaPlayer.PSMPCallback callback = new PlaybackServiceMediaPlayer.PSMPCallback() {
+ @Override
+ public void statusChanged(PlaybackServiceMediaPlayer.PSMPInfo newInfo) {
+ try {
+ checkPSMPInfo(newInfo);
+ if (newInfo.playerStatus == PlayerStatus.ERROR)
+ throw new IllegalStateException("MediaPlayer error");
+ if (countDownLatch.getCount() == 0) {
+ fail();
+
+ } else if (countDownLatch.getCount() == 5) {
+ assertEquals(PlayerStatus.INITIALIZING, newInfo.playerStatus);
+ } else if (countDownLatch.getCount() == 4) {
+ assertEquals(PlayerStatus.INITIALIZED, newInfo.playerStatus);
+ } else if (countDownLatch.getCount() == 3) {
+ assertEquals(PlayerStatus.PREPARING, newInfo.playerStatus);
+ } else if (countDownLatch.getCount() == 2) {
+ assertEquals(PlayerStatus.PREPARED, newInfo.playerStatus);
+ } else if (countDownLatch.getCount() == 1) {
+ assertEquals(PlayerStatus.PLAYING, newInfo.playerStatus);
+ }
+ countDownLatch.countDown();
+ } catch (AssertionFailedError e) {
+ if (assertionError == null)
+ assertionError = e;
+ }
+ }
+
+ @Override
+ public void shouldStop() {
+
+ }
+
+ @Override
+ public void playbackSpeedChanged(float s) {
+
+ }
+
+ @Override
+ public void onBufferingUpdate(int percent) {
+
+ }
+
+ @Override
+ public boolean onMediaPlayerInfo(int code) {
+ return false;
+ }
+
+ @Override
+ public boolean onMediaPlayerError(Object inObj, int what, int extra) {
+ return false;
+ }
+
+ @Override
+ public boolean endPlayback(boolean playNextEpisode) {
+ return false;
+ }
+
+ @Override
+ public RemoteControlClient getRemoteControlClient() {
+ return null;
+ }
+ };
+ PlaybackServiceMediaPlayer psmp = new PlaybackServiceMediaPlayer(c, callback);
+ Playable p = writeTestPlayable(PLAYABLE_FILE_URL, null);
+ psmp.playMediaObject(p, true, true, true);
+ boolean res = countDownLatch.await(LATCH_TIMEOUT_SECONDS, TimeUnit.SECONDS);
+ if (assertionError != null)
+ throw assertionError;
+ assertTrue(res);
+ assertTrue(psmp.getPSMPInfo().playerStatus == PlayerStatus.PLAYING);
+ psmp.shutdown();
+ }
+
+ public void testPlayMediaObjectLocalNoStartNoPrepare() throws InterruptedException {
+ final Context c = getInstrumentation().getTargetContext();
+ final CountDownLatch countDownLatch = new CountDownLatch(2);
+ PlaybackServiceMediaPlayer.PSMPCallback callback = new PlaybackServiceMediaPlayer.PSMPCallback() {
+ @Override
+ public void statusChanged(PlaybackServiceMediaPlayer.PSMPInfo newInfo) {
+ try {
+ checkPSMPInfo(newInfo);
+ if (newInfo.playerStatus == PlayerStatus.ERROR)
+ throw new IllegalStateException("MediaPlayer error");
+ if (countDownLatch.getCount() == 0) {
+ fail();
+ } else if (countDownLatch.getCount() == 2) {
+ assertEquals(PlayerStatus.INITIALIZING, newInfo.playerStatus);
+ countDownLatch.countDown();
+ } else {
+ assertEquals(PlayerStatus.INITIALIZED, newInfo.playerStatus);
+ countDownLatch.countDown();
+ }
+ } catch (AssertionFailedError e) {
+ if (assertionError == null)
+ assertionError = e;
+ }
+ }
+
+ @Override
+ public void shouldStop() {
+
+ }
+
+ @Override
+ public void playbackSpeedChanged(float s) {
+
+ }
+
+ @Override
+ public void onBufferingUpdate(int percent) {
+
+ }
+
+ @Override
+ public boolean onMediaPlayerInfo(int code) {
+ return false;
+ }
+
+ @Override
+ public boolean onMediaPlayerError(Object inObj, int what, int extra) {
+ return false;
+ }
+
+ @Override
+ public boolean endPlayback(boolean playNextEpisode) {
+ return false;
+ }
+
+ @Override
+ public RemoteControlClient getRemoteControlClient() {
+ return null;
+ }
+ };
+ PlaybackServiceMediaPlayer psmp = new PlaybackServiceMediaPlayer(c, callback);
+ Playable p = writeTestPlayable(PLAYABLE_FILE_URL, PLAYABLE_LOCAL_URL);
+ psmp.playMediaObject(p, false, false, false);
+ boolean res = countDownLatch.await(LATCH_TIMEOUT_SECONDS, TimeUnit.SECONDS);
+ if (assertionError != null)
+ throw assertionError;
+ assertTrue(res);
+ assertTrue(psmp.getPSMPInfo().playerStatus == PlayerStatus.INITIALIZED);
+ assertFalse(psmp.isStartWhenPrepared());
+ psmp.shutdown();
+ }
+
+ public void testPlayMediaObjectLocalStartNoPrepare() throws InterruptedException {
+ final Context c = getInstrumentation().getTargetContext();
+ final CountDownLatch countDownLatch = new CountDownLatch(2);
+ PlaybackServiceMediaPlayer.PSMPCallback callback = new PlaybackServiceMediaPlayer.PSMPCallback() {
+ @Override
+ public void statusChanged(PlaybackServiceMediaPlayer.PSMPInfo newInfo) {
+ try {
+ checkPSMPInfo(newInfo);
+ if (newInfo.playerStatus == PlayerStatus.ERROR)
+ throw new IllegalStateException("MediaPlayer error");
+ if (countDownLatch.getCount() == 0) {
+ fail();
+ } else if (countDownLatch.getCount() == 2) {
+ assertEquals(PlayerStatus.INITIALIZING, newInfo.playerStatus);
+ countDownLatch.countDown();
+ } else {
+ assertEquals(PlayerStatus.INITIALIZED, newInfo.playerStatus);
+ countDownLatch.countDown();
+ }
+ } catch (AssertionFailedError e) {
+ if (assertionError == null)
+ assertionError = e;
+ }
+ }
+
+ @Override
+ public void shouldStop() {
+
+ }
+
+ @Override
+ public void playbackSpeedChanged(float s) {
+
+ }
+
+ @Override
+ public void onBufferingUpdate(int percent) {
+
+ }
+
+ @Override
+ public boolean onMediaPlayerInfo(int code) {
+ return false;
+ }
+
+ @Override
+ public boolean onMediaPlayerError(Object inObj, int what, int extra) {
+ return false;
+ }
+
+ @Override
+ public boolean endPlayback(boolean playNextEpisode) {
+ return false;
+ }
+
+ @Override
+ public RemoteControlClient getRemoteControlClient() {
+ return null;
+ }
+ };
+ PlaybackServiceMediaPlayer psmp = new PlaybackServiceMediaPlayer(c, callback);
+ Playable p = writeTestPlayable(PLAYABLE_FILE_URL, PLAYABLE_LOCAL_URL);
+ psmp.playMediaObject(p, false, true, false);
+ boolean res = countDownLatch.await(LATCH_TIMEOUT_SECONDS, TimeUnit.SECONDS);
+ if (assertionError != null)
+ throw assertionError;
+ assertTrue(res);
+ assertTrue(psmp.getPSMPInfo().playerStatus == PlayerStatus.INITIALIZED);
+ assertTrue(psmp.isStartWhenPrepared());
+ psmp.shutdown();
+ }
+
+ public void testPlayMediaObjectLocalNoStartPrepare() throws InterruptedException {
+ final Context c = getInstrumentation().getTargetContext();
+ final CountDownLatch countDownLatch = new CountDownLatch(4);
+ PlaybackServiceMediaPlayer.PSMPCallback callback = new PlaybackServiceMediaPlayer.PSMPCallback() {
+ @Override
+ public void statusChanged(PlaybackServiceMediaPlayer.PSMPInfo newInfo) {
+ try {
+ checkPSMPInfo(newInfo);
+ if (newInfo.playerStatus == PlayerStatus.ERROR)
+ throw new IllegalStateException("MediaPlayer error");
+ if (countDownLatch.getCount() == 0) {
+ fail();
+ } else if (countDownLatch.getCount() == 4) {
+ assertEquals(PlayerStatus.INITIALIZING, newInfo.playerStatus);
+ } else if (countDownLatch.getCount() == 3) {
+ assertEquals(PlayerStatus.INITIALIZED, newInfo.playerStatus);
+ } else if (countDownLatch.getCount() == 2) {
+ assertEquals(PlayerStatus.PREPARING, newInfo.playerStatus);
+ } else if (countDownLatch.getCount() == 1) {
+ assertEquals(PlayerStatus.PREPARED, newInfo.playerStatus);
+ }
+ countDownLatch.countDown();
+ } catch (AssertionFailedError e) {
+ if (assertionError == null)
+ assertionError = e;
+ }
+ }
+
+ @Override
+ public void shouldStop() {
+
+ }
+
+ @Override
+ public void playbackSpeedChanged(float s) {
+
+ }
+
+ @Override
+ public void onBufferingUpdate(int percent) {
+
+ }
+
+ @Override
+ public boolean onMediaPlayerInfo(int code) {
+ return false;
+ }
+
+ @Override
+ public boolean onMediaPlayerError(Object inObj, int what, int extra) {
+ return false;
+ }
+
+ @Override
+ public boolean endPlayback(boolean playNextEpisode) {
+ return false;
+ }
+
+ @Override
+ public RemoteControlClient getRemoteControlClient() {
+ return null;
+ }
+ };
+ PlaybackServiceMediaPlayer psmp = new PlaybackServiceMediaPlayer(c, callback);
+ Playable p = writeTestPlayable(PLAYABLE_FILE_URL, PLAYABLE_LOCAL_URL);
+ psmp.playMediaObject(p, false, false, true);
+ boolean res = countDownLatch.await(LATCH_TIMEOUT_SECONDS, TimeUnit.SECONDS);
+ if (assertionError != null)
+ throw assertionError;
+ assertTrue(res);
+ assertTrue(psmp.getPSMPInfo().playerStatus == PlayerStatus.PREPARED);
+ psmp.shutdown();
+ }
+
+ public void testPlayMediaObjectLocalStartPrepare() throws InterruptedException {
+ final Context c = getInstrumentation().getTargetContext();
+ final CountDownLatch countDownLatch = new CountDownLatch(5);
+ PlaybackServiceMediaPlayer.PSMPCallback callback = new PlaybackServiceMediaPlayer.PSMPCallback() {
+ @Override
+ public void statusChanged(PlaybackServiceMediaPlayer.PSMPInfo newInfo) {
+ try {
+ checkPSMPInfo(newInfo);
+ if (newInfo.playerStatus == PlayerStatus.ERROR)
+ throw new IllegalStateException("MediaPlayer error");
+ if (countDownLatch.getCount() == 0) {
+ fail();
+ } else if (countDownLatch.getCount() == 5) {
+ assertEquals(PlayerStatus.INITIALIZING, newInfo.playerStatus);
+ } else if (countDownLatch.getCount() == 4) {
+ assertEquals(PlayerStatus.INITIALIZED, newInfo.playerStatus);
+ } else if (countDownLatch.getCount() == 3) {
+ assertEquals(PlayerStatus.PREPARING, newInfo.playerStatus);
+ } else if (countDownLatch.getCount() == 2) {
+ assertEquals(PlayerStatus.PREPARED, newInfo.playerStatus);
+ } else if (countDownLatch.getCount() == 1) {
+ assertEquals(PlayerStatus.PLAYING, newInfo.playerStatus);
+ }
+
+ } catch (AssertionFailedError e) {
+ if (assertionError == null)
+ assertionError = e;
+ } finally {
+ countDownLatch.countDown();
+ }
+ }
+
+ @Override
+ public void shouldStop() {
+
+ }
+
+ @Override
+ public void playbackSpeedChanged(float s) {
+
+ }
+
+ @Override
+ public void onBufferingUpdate(int percent) {
+
+ }
+
+ @Override
+ public boolean onMediaPlayerInfo(int code) {
+ return false;
+ }
+
+ @Override
+ public boolean onMediaPlayerError(Object inObj, int what, int extra) {
+ return false;
+ }
+
+ @Override
+ public boolean endPlayback(boolean playNextEpisode) {
+ return false;
+ }
+
+ @Override
+ public RemoteControlClient getRemoteControlClient() {
+ return null;
+ }
+ };
+ PlaybackServiceMediaPlayer psmp = new PlaybackServiceMediaPlayer(c, callback);
+ Playable p = writeTestPlayable(PLAYABLE_FILE_URL, PLAYABLE_LOCAL_URL);
+ psmp.playMediaObject(p, false, true, true);
+ boolean res = countDownLatch.await(LATCH_TIMEOUT_SECONDS, TimeUnit.SECONDS);
+ if (assertionError != null)
+ throw assertionError;
+ assertTrue(res);
+ assertTrue(psmp.getPSMPInfo().playerStatus == PlayerStatus.PLAYING);
+ psmp.shutdown();
+ }
+
+
+ private final PlaybackServiceMediaPlayer.PSMPCallback defaultCallback = new PlaybackServiceMediaPlayer.PSMPCallback() {
+ @Override
+ public void statusChanged(PlaybackServiceMediaPlayer.PSMPInfo newInfo) {
+ checkPSMPInfo(newInfo);
+ }
+
+ @Override
+ public void shouldStop() {
+
+ }
+
+ @Override
+ public void playbackSpeedChanged(float s) {
+
+ }
+
+ @Override
+ public void onBufferingUpdate(int percent) {
+
+ }
+
+ @Override
+ public boolean onMediaPlayerInfo(int code) {
+ return false;
+ }
+
+ @Override
+ public boolean onMediaPlayerError(Object inObj, int what, int extra) {
+ return false;
+ }
+
+ @Override
+ public boolean endPlayback(boolean playNextEpisode) {
+ return false;
+ }
+
+ @Override
+ public RemoteControlClient getRemoteControlClient() {
+ return null;
+ }
+ };
+
+ private void pauseTestSkeleton(final PlayerStatus initialState, final boolean stream, final boolean abandonAudioFocus, final boolean reinit, long timeoutSeconds) throws InterruptedException {
+ final Context c = getInstrumentation().getTargetContext();
+ final int latchCount = (stream && reinit) ? 2 : 1;
+ final CountDownLatch countDownLatch = new CountDownLatch(latchCount);
+
+ PlaybackServiceMediaPlayer.PSMPCallback callback = new PlaybackServiceMediaPlayer.PSMPCallback() {
+ @Override
+ public void statusChanged(PlaybackServiceMediaPlayer.PSMPInfo newInfo) {
+ checkPSMPInfo(newInfo);
+ if (newInfo.playerStatus == PlayerStatus.ERROR) {
+ if (assertionError == null)
+ assertionError = new UnexpectedStateChange(newInfo.playerStatus);
+ } else if (initialState != PlayerStatus.PLAYING) {
+ if (assertionError == null)
+ assertionError = new UnexpectedStateChange(newInfo.playerStatus);
+ } else {
+ switch (newInfo.playerStatus) {
+ case PAUSED:
+ if (latchCount == countDownLatch.getCount())
+ countDownLatch.countDown();
+ else {
+ if (assertionError == null)
+ assertionError = new UnexpectedStateChange(newInfo.playerStatus);
+ }
+ break;
+ case INITIALIZED:
+ if (stream && reinit && countDownLatch.getCount() < latchCount) {
+ countDownLatch.countDown();
+ } else if (countDownLatch.getCount() < latchCount) {
+ if (assertionError == null)
+ assertionError = new UnexpectedStateChange(newInfo.playerStatus);
+ }
+ break;
+ }
+ }
+
+ }
+
+ @Override
+ public void shouldStop() {
+ if (assertionError == null)
+ assertionError = new AssertionFailedError("Unexpected call to shouldStop");
+ }
+
+ @Override
+ public void playbackSpeedChanged(float s) {
+
+ }
+
+ @Override
+ public void onBufferingUpdate(int percent) {
+
+ }
+
+ @Override
+ public boolean onMediaPlayerInfo(int code) {
+ return false;
+ }
+
+ @Override
+ public boolean onMediaPlayerError(Object inObj, int what, int extra) {
+ if (assertionError == null)
+ assertionError = new AssertionFailedError("Unexpected call to onMediaPlayerError");
+ return false;
+ }
+
+ @Override
+ public boolean endPlayback(boolean playNextEpisode) {
+ return false;
+ }
+
+ @Override
+ public RemoteControlClient getRemoteControlClient() {
+ return null;
+ }
+ };
+ PlaybackServiceMediaPlayer psmp = new PlaybackServiceMediaPlayer(c, callback);
+ Playable p = writeTestPlayable(PLAYABLE_FILE_URL, PLAYABLE_LOCAL_URL);
+ if (initialState == PlayerStatus.PLAYING) {
+ psmp.playMediaObject(p, stream, true, true);
+ }
+ psmp.pause(abandonAudioFocus, reinit);
+ boolean res = countDownLatch.await(timeoutSeconds, TimeUnit.SECONDS);
+ if (assertionError != null)
+ throw assertionError;
+ assertTrue(res || initialState != PlayerStatus.PLAYING);
+ psmp.shutdown();
+ }
+
+ public void testPauseDefaultState() throws InterruptedException {
+ pauseTestSkeleton(PlayerStatus.STOPPED, false, false, false, 1);
+ }
+
+ public void testPausePlayingStateNoAbandonNoReinitNoStream() throws InterruptedException {
+ pauseTestSkeleton(PlayerStatus.PLAYING, false, false, false, LATCH_TIMEOUT_SECONDS);
+ }
+
+ public void testPausePlayingStateNoAbandonNoReinitStream() throws InterruptedException {
+ pauseTestSkeleton(PlayerStatus.PLAYING, true, false, false, LATCH_TIMEOUT_SECONDS);
+ }
+
+ public void testPausePlayingStateAbandonNoReinitNoStream() throws InterruptedException {
+ pauseTestSkeleton(PlayerStatus.PLAYING, false, true, false, LATCH_TIMEOUT_SECONDS);
+ }
+
+ public void testPausePlayingStateAbandonNoReinitStream() throws InterruptedException {
+ pauseTestSkeleton(PlayerStatus.PLAYING, true, true, false, LATCH_TIMEOUT_SECONDS);
+ }
+
+ public void testPausePlayingStateNoAbandonReinitNoStream() throws InterruptedException {
+ pauseTestSkeleton(PlayerStatus.PLAYING, false, false, true, LATCH_TIMEOUT_SECONDS);
+ }
+
+ public void testPausePlayingStateNoAbandonReinitStream() throws InterruptedException {
+ pauseTestSkeleton(PlayerStatus.PLAYING, true, false, true, LATCH_TIMEOUT_SECONDS);
+ }
+
+ public void testPausePlayingStateAbandonReinitNoStream() throws InterruptedException {
+ pauseTestSkeleton(PlayerStatus.PLAYING, false, true, true, LATCH_TIMEOUT_SECONDS);
+ }
+
+ public void testPausePlayingStateAbandonReinitStream() throws InterruptedException {
+ pauseTestSkeleton(PlayerStatus.PLAYING, true, true, true, LATCH_TIMEOUT_SECONDS);
+ }
+
+ private void resumeTestSkeleton(final PlayerStatus initialState, long timeoutSeconds) throws InterruptedException {
+ final Context c = getInstrumentation().getTargetContext();
+ final int latchCount = (initialState == PlayerStatus.PAUSED || initialState == PlayerStatus.PLAYING) ? 2 :
+ (initialState == PlayerStatus.PREPARED) ? 1 : 0;
+ final CountDownLatch countDownLatch = new CountDownLatch(latchCount);
+
+ PlaybackServiceMediaPlayer.PSMPCallback callback = new PlaybackServiceMediaPlayer.PSMPCallback() {
+ @Override
+ public void statusChanged(PlaybackServiceMediaPlayer.PSMPInfo newInfo) {
+ checkPSMPInfo(newInfo);
+ if (newInfo.playerStatus == PlayerStatus.ERROR) {
+ if (assertionError == null)
+ assertionError = new UnexpectedStateChange(newInfo.playerStatus);
+ } else if (newInfo.playerStatus == PlayerStatus.PLAYING) {
+ if (countDownLatch.getCount() == 0) {
+ if (assertionError == null)
+ assertionError = new UnexpectedStateChange(newInfo.playerStatus);
+ } else {
+ countDownLatch.countDown();
+ }
+ }
+
+ }
+
+ @Override
+ public void shouldStop() {
+
+ }
+
+ @Override
+ public void playbackSpeedChanged(float s) {
+
+ }
+
+ @Override
+ public void onBufferingUpdate(int percent) {
+
+ }
+
+ @Override
+ public boolean onMediaPlayerInfo(int code) {
+ return false;
+ }
+
+ @Override
+ public boolean onMediaPlayerError(Object inObj, int what, int extra) {
+ if (assertionError == null) {
+ assertionError = new AssertionFailedError("Unexpected call of onMediaPlayerError");
+ }
+ return false;
+ }
+
+ @Override
+ public boolean endPlayback(boolean playNextEpisode) {
+ return false;
+ }
+
+ @Override
+ public RemoteControlClient getRemoteControlClient() {
+ return null;
+ }
+ };
+ PlaybackServiceMediaPlayer psmp = new PlaybackServiceMediaPlayer(c, callback);
+ if (initialState == PlayerStatus.PREPARED || initialState == PlayerStatus.PLAYING || initialState == PlayerStatus.PAUSED) {
+ boolean startWhenPrepared = (initialState != PlayerStatus.PREPARED);
+ psmp.playMediaObject(writeTestPlayable(PLAYABLE_FILE_URL, PLAYABLE_LOCAL_URL), false, startWhenPrepared, true);
+ }
+ if (initialState == PlayerStatus.PAUSED) {
+ psmp.pause(false, false);
+ }
+ psmp.resume();
+ boolean res = countDownLatch.await(timeoutSeconds, TimeUnit.SECONDS);
+ if (assertionError != null)
+ throw assertionError;
+ assertTrue(res || (initialState != PlayerStatus.PAUSED && initialState != PlayerStatus.PREPARED));
+ psmp.shutdown();
+ }
+
+ public void testResumePausedState() throws InterruptedException {
+ resumeTestSkeleton(PlayerStatus.PAUSED, LATCH_TIMEOUT_SECONDS);
+ }
+
+ public void testResumePreparedState() throws InterruptedException {
+ resumeTestSkeleton(PlayerStatus.PREPARED, LATCH_TIMEOUT_SECONDS);
+ }
+
+ public void testResumePlayingState() throws InterruptedException {
+ resumeTestSkeleton(PlayerStatus.PLAYING, 1);
+ }
+
+ private void prepareTestSkeleton(final PlayerStatus initialState, long timeoutSeconds) throws InterruptedException {
+ final Context c = getInstrumentation().getTargetContext();
+ final int latchCount = 1;
+ final CountDownLatch countDownLatch = new CountDownLatch(latchCount);
+ PlaybackServiceMediaPlayer.PSMPCallback callback = new PlaybackServiceMediaPlayer.PSMPCallback() {
+ @Override
+ public void statusChanged(PlaybackServiceMediaPlayer.PSMPInfo newInfo) {
+ checkPSMPInfo(newInfo);
+ if (newInfo.playerStatus == PlayerStatus.ERROR) {
+ if (assertionError == null)
+ assertionError = new UnexpectedStateChange(newInfo.playerStatus);
+ } else {
+ if (initialState == PlayerStatus.INITIALIZED && newInfo.playerStatus == PlayerStatus.PREPARED) {
+ countDownLatch.countDown();
+ } else if (initialState != PlayerStatus.INITIALIZED && initialState == newInfo.playerStatus) {
+ countDownLatch.countDown();
+ }
+ }
+
+ }
+
+ @Override
+ public void shouldStop() {
+
+ }
+
+ @Override
+ public void playbackSpeedChanged(float s) {
+
+ }
+
+ @Override
+ public void onBufferingUpdate(int percent) {
+
+ }
+
+ @Override
+ public boolean onMediaPlayerInfo(int code) {
+ return false;
+ }
+
+ @Override
+ public boolean onMediaPlayerError(Object inObj, int what, int extra) {
+ if (assertionError == null)
+ assertionError = new AssertionFailedError("Unexpected call to onMediaPlayerError");
+ return false;
+ }
+
+ @Override
+ public boolean endPlayback(boolean playNextEpisode) {
+ return false;
+ }
+
+ @Override
+ public RemoteControlClient getRemoteControlClient() {
+ return null;
+ }
+ };
+ PlaybackServiceMediaPlayer psmp = new PlaybackServiceMediaPlayer(c, callback);
+ Playable p = writeTestPlayable(PLAYABLE_FILE_URL, PLAYABLE_LOCAL_URL);
+ if (initialState == PlayerStatus.INITIALIZED
+ || initialState == PlayerStatus.PLAYING
+ || initialState == PlayerStatus.PREPARED
+ || initialState == PlayerStatus.PAUSED) {
+ boolean prepareImmediately = (initialState != PlayerStatus.INITIALIZED);
+ boolean startWhenPrepared = (initialState != PlayerStatus.PREPARED);
+ psmp.playMediaObject(p, false, startWhenPrepared, prepareImmediately);
+ if (initialState == PlayerStatus.PAUSED) {
+ psmp.pause(false, false);
+ }
+ psmp.prepare();
+ }
+
+ boolean res = countDownLatch.await(timeoutSeconds, TimeUnit.SECONDS);
+ if (initialState != PlayerStatus.INITIALIZED) {
+ assertEquals(initialState, psmp.getPSMPInfo().playerStatus);
+ }
+
+ if (assertionError != null)
+ throw assertionError;
+ assertTrue(res);
+ psmp.shutdown();
+ }
+
+ public void testPrepareInitializedState() throws InterruptedException {
+ prepareTestSkeleton(PlayerStatus.INITIALIZED, LATCH_TIMEOUT_SECONDS);
+ }
+
+ public void testPreparePlayingState() throws InterruptedException {
+ prepareTestSkeleton(PlayerStatus.PLAYING, 1);
+ }
+
+ public void testPreparePausedState() throws InterruptedException {
+ prepareTestSkeleton(PlayerStatus.PAUSED, 1);
+ }
+
+ public void testPreparePreparedState() throws InterruptedException {
+ prepareTestSkeleton(PlayerStatus.PREPARED, 1);
+ }
+
+ private void reinitTestSkeleton(final PlayerStatus initialState, final long timeoutSeconds) throws InterruptedException {
+ final Context c = getInstrumentation().getTargetContext();
+ final int latchCount = 2;
+ final CountDownLatch countDownLatch = new CountDownLatch(latchCount);
+ PlaybackServiceMediaPlayer.PSMPCallback callback = new PlaybackServiceMediaPlayer.PSMPCallback() {
+ @Override
+ public void statusChanged(PlaybackServiceMediaPlayer.PSMPInfo newInfo) {
+ checkPSMPInfo(newInfo);
+ if (newInfo.playerStatus == PlayerStatus.ERROR) {
+ if (assertionError == null)
+ assertionError = new UnexpectedStateChange(newInfo.playerStatus);
+ } else {
+ if (newInfo.playerStatus == initialState) {
+ countDownLatch.countDown();
+ } else if (countDownLatch.getCount() < latchCount && newInfo.playerStatus == PlayerStatus.INITIALIZED) {
+ countDownLatch.countDown();
+ }
+ }
+ }
+
+ @Override
+ public void shouldStop() {
+
+ }
+
+ @Override
+ public void playbackSpeedChanged(float s) {
+
+ }
+
+ @Override
+ public void onBufferingUpdate(int percent) {
+
+ }
+
+ @Override
+ public boolean onMediaPlayerInfo(int code) {
+ return false;
+ }
+
+ @Override
+ public boolean onMediaPlayerError(Object inObj, int what, int extra) {
+ if (assertionError == null)
+ assertionError = new AssertionFailedError("Unexpected call to onMediaPlayerError");
+ return false;
+ }
+
+ @Override
+ public boolean endPlayback(boolean playNextEpisode) {
+ return false;
+ }
+
+ @Override
+ public RemoteControlClient getRemoteControlClient() {
+ return null;
+ }
+ };
+ PlaybackServiceMediaPlayer psmp = new PlaybackServiceMediaPlayer(c, callback);
+ Playable p = writeTestPlayable(PLAYABLE_FILE_URL, PLAYABLE_LOCAL_URL);
+ boolean prepareImmediately = initialState != PlayerStatus.INITIALIZED;
+ boolean startImmediately = initialState != PlayerStatus.PREPARED;
+ psmp.playMediaObject(p, false, startImmediately, prepareImmediately);
+ if (initialState == PlayerStatus.PAUSED) {
+ psmp.pause(false, false);
+ }
+ psmp.reinit();
+ boolean res = countDownLatch.await(timeoutSeconds, TimeUnit.SECONDS);
+ if (assertionError != null)
+ throw assertionError;
+ assertTrue(res);
+ psmp.shutdown();
+ }
+
+ public void testReinitPlayingState() throws InterruptedException {
+ reinitTestSkeleton(PlayerStatus.PLAYING, LATCH_TIMEOUT_SECONDS);
+ }
+
+ public void testReinitPausedState() throws InterruptedException {
+ reinitTestSkeleton(PlayerStatus.PAUSED, LATCH_TIMEOUT_SECONDS);
+ }
+
+ public void testPreparedPlayingState() throws InterruptedException {
+ reinitTestSkeleton(PlayerStatus.PREPARED, LATCH_TIMEOUT_SECONDS);
+ }
+
+ public void testReinitInitializedState() throws InterruptedException {
+ reinitTestSkeleton(PlayerStatus.INITIALIZED, LATCH_TIMEOUT_SECONDS);
+ }
+
+ private static class UnexpectedStateChange extends AssertionFailedError {
+ public UnexpectedStateChange(PlayerStatus status) {
+ super("Unexpected state change: " + status);
+ }
+ }
+}
diff --git a/src/instrumentationTest/de/test/antennapod/service/playback/PlaybackServiceTaskManagerTest.java b/src/instrumentationTest/de/test/antennapod/service/playback/PlaybackServiceTaskManagerTest.java
new file mode 100644
index 000000000..19f64b4cf
--- /dev/null
+++ b/src/instrumentationTest/de/test/antennapod/service/playback/PlaybackServiceTaskManagerTest.java
@@ -0,0 +1,333 @@
+package instrumentationTest.de.test.antennapod.service.playback;
+
+import android.content.Context;
+import android.test.InstrumentationTestCase;
+import de.danoeh.antennapod.feed.EventDistributor;
+import de.danoeh.antennapod.feed.Feed;
+import de.danoeh.antennapod.feed.FeedItem;
+import de.danoeh.antennapod.service.playback.PlaybackServiceTaskManager;
+import de.danoeh.antennapod.storage.PodDBAdapter;
+import de.danoeh.antennapod.util.playback.Playable;
+
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Test class for PlaybackServiceTaskManager
+ */
+public class PlaybackServiceTaskManagerTest extends InstrumentationTestCase {
+
+ @Override
+ protected void tearDown() throws Exception {
+ super.tearDown();
+ assertTrue(PodDBAdapter.deleteDatabase(getInstrumentation().getTargetContext()));
+ }
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ final Context context = getInstrumentation().getTargetContext();
+ context.deleteDatabase(PodDBAdapter.DATABASE_NAME);
+ // make sure database is created
+ PodDBAdapter adapter = new PodDBAdapter(context);
+ adapter.open();
+ adapter.close();
+ }
+
+ public void testInit() {
+ PlaybackServiceTaskManager pstm = new PlaybackServiceTaskManager(getInstrumentation().getTargetContext(), defaultPSTM);
+ pstm.shutdown();
+ }
+
+ private List writeTestQueue(String pref) {
+ final Context c = getInstrumentation().getTargetContext();
+ final int NUM_ITEMS = 10;
+ Feed f = new Feed(0, new Date(), "title", "link", "d", null, null, null, null, "id", null, "null", "url", false);
+ f.setItems(new ArrayList());
+ for (int i = 0; i < NUM_ITEMS; i++) {
+ f.getItems().add(new FeedItem(0, pref + i, pref + i, "link", new Date(), true, f));
+ }
+ PodDBAdapter adapter = new PodDBAdapter(c);
+ adapter.open();
+ adapter.setCompleteFeed(f);
+ adapter.setQueue(f.getItems());
+ adapter.close();
+
+ for (FeedItem item : f.getItems()) {
+ assertTrue(item.getId() != 0);
+ }
+ return f.getItems();
+ }
+
+ public void testGetQueueWriteBeforeCreation() throws InterruptedException {
+ final Context c = getInstrumentation().getTargetContext();
+ List queue = writeTestQueue("a");
+ assertNotNull(queue);
+ PlaybackServiceTaskManager pstm = new PlaybackServiceTaskManager(c, defaultPSTM);
+ List testQueue = pstm.getQueue();
+ assertNotNull(testQueue);
+ assertTrue(queue.size() == testQueue.size());
+ for (int i = 0; i < queue.size(); i++) {
+ assertTrue(queue.get(i).getId() == testQueue.get(i).getId());
+ }
+ pstm.shutdown();
+ }
+
+ public void testGetQueueWriteAfterCreation() throws InterruptedException {
+ final Context c = getInstrumentation().getTargetContext();
+
+ PlaybackServiceTaskManager pstm = new PlaybackServiceTaskManager(c, defaultPSTM);
+ List testQueue = pstm.getQueue();
+ assertNotNull(testQueue);
+ assertTrue(testQueue.isEmpty());
+
+
+ final CountDownLatch countDownLatch = new CountDownLatch(1);
+ EventDistributor.EventListener queueListener = new EventDistributor.EventListener() {
+ @Override
+ public void update(EventDistributor eventDistributor, Integer arg) {
+ countDownLatch.countDown();
+ }
+ };
+ EventDistributor.getInstance().register(queueListener);
+ List queue = writeTestQueue("a");
+ EventDistributor.getInstance().sendQueueUpdateBroadcast();
+ countDownLatch.await(5000, TimeUnit.MILLISECONDS);
+
+ assertNotNull(queue);
+ testQueue = pstm.getQueue();
+ assertNotNull(testQueue);
+ assertTrue(queue.size() == testQueue.size());
+ for (int i = 0; i < queue.size(); i++) {
+ assertTrue(queue.get(i).getId() == testQueue.get(i).getId());
+ }
+ pstm.shutdown();
+ }
+
+ public void testStartPositionSaver() throws InterruptedException {
+ final Context c = getInstrumentation().getTargetContext();
+ final int NUM_COUNTDOWNS = 2;
+ final int TIMEOUT = 3 * PlaybackServiceTaskManager.POSITION_SAVER_WAITING_INTERVAL;
+ final CountDownLatch countDownLatch = new CountDownLatch(NUM_COUNTDOWNS);
+ PlaybackServiceTaskManager pstm = new PlaybackServiceTaskManager(c, new PlaybackServiceTaskManager.PSTMCallback() {
+ @Override
+ public void positionSaverTick() {
+ countDownLatch.countDown();
+ }
+
+ @Override
+ public void onSleepTimerExpired() {
+
+ }
+
+ @Override
+ public void onWidgetUpdaterTick() {
+
+ }
+
+ @Override
+ public void onChapterLoaded(Playable media) {
+
+ }
+ });
+ pstm.startPositionSaver();
+ countDownLatch.await(TIMEOUT, TimeUnit.MILLISECONDS);
+ pstm.shutdown();
+ }
+
+ public void testIsPositionSaverActive() {
+ final Context c = getInstrumentation().getTargetContext();
+ PlaybackServiceTaskManager pstm = new PlaybackServiceTaskManager(c, defaultPSTM);
+ pstm.startPositionSaver();
+ assertTrue(pstm.isPositionSaverActive());
+ pstm.shutdown();
+ }
+
+ public void testCancelPositionSaver() {
+ final Context c = getInstrumentation().getTargetContext();
+ PlaybackServiceTaskManager pstm = new PlaybackServiceTaskManager(c, defaultPSTM);
+ pstm.startPositionSaver();
+ pstm.cancelPositionSaver();
+ assertFalse(pstm.isPositionSaverActive());
+ pstm.shutdown();
+ }
+
+ public void testStartWidgetUpdater() throws InterruptedException {
+ final Context c = getInstrumentation().getTargetContext();
+ final int NUM_COUNTDOWNS = 2;
+ final int TIMEOUT = 3 * PlaybackServiceTaskManager.WIDGET_UPDATER_NOTIFICATION_INTERVAL;
+ final CountDownLatch countDownLatch = new CountDownLatch(NUM_COUNTDOWNS);
+ PlaybackServiceTaskManager pstm = new PlaybackServiceTaskManager(c, new PlaybackServiceTaskManager.PSTMCallback() {
+ @Override
+ public void positionSaverTick() {
+
+ }
+
+ @Override
+ public void onSleepTimerExpired() {
+
+ }
+
+ @Override
+ public void onWidgetUpdaterTick() {
+ countDownLatch.countDown();
+ }
+
+ @Override
+ public void onChapterLoaded(Playable media) {
+
+ }
+ });
+ pstm.startWidgetUpdater();
+ countDownLatch.await(TIMEOUT, TimeUnit.MILLISECONDS);
+ pstm.shutdown();
+ }
+
+ public void testIsWidgetUpdaterActive() {
+ final Context c = getInstrumentation().getTargetContext();
+ PlaybackServiceTaskManager pstm = new PlaybackServiceTaskManager(c, defaultPSTM);
+ pstm.startWidgetUpdater();
+ assertTrue(pstm.isWidgetUpdaterActive());
+ pstm.shutdown();
+ }
+
+ public void testCancelWidgetUpdater() {
+ final Context c = getInstrumentation().getTargetContext();
+ PlaybackServiceTaskManager pstm = new PlaybackServiceTaskManager(c, defaultPSTM);
+ pstm.startWidgetUpdater();
+ pstm.cancelWidgetUpdater();
+ assertFalse(pstm.isWidgetUpdaterActive());
+ pstm.shutdown();
+ }
+
+ public void testCancelAllTasksNoTasksStarted() {
+ final Context c = getInstrumentation().getTargetContext();
+ PlaybackServiceTaskManager pstm = new PlaybackServiceTaskManager(c, defaultPSTM);
+ pstm.cancelAllTasks();
+ assertFalse(pstm.isPositionSaverActive());
+ assertFalse(pstm.isWidgetUpdaterActive());
+ assertFalse(pstm.isSleepTimerActive());
+ pstm.shutdown();
+ }
+
+ public void testCancelAllTasksAllTasksStarted() {
+ final Context c = getInstrumentation().getTargetContext();
+ PlaybackServiceTaskManager pstm = new PlaybackServiceTaskManager(c, defaultPSTM);
+ pstm.startWidgetUpdater();
+ pstm.startPositionSaver();
+ pstm.setSleepTimer(100000);
+ pstm.cancelAllTasks();
+ assertFalse(pstm.isPositionSaverActive());
+ assertFalse(pstm.isWidgetUpdaterActive());
+ assertFalse(pstm.isSleepTimerActive());
+ pstm.shutdown();
+ }
+
+ public void testSetSleepTimer() throws InterruptedException {
+ final Context c = getInstrumentation().getTargetContext();
+ final long TIME = 2000;
+ final long TIMEOUT = 2 * TIME;
+ final CountDownLatch countDownLatch = new CountDownLatch(1);
+ PlaybackServiceTaskManager pstm = new PlaybackServiceTaskManager(c, new PlaybackServiceTaskManager.PSTMCallback() {
+ @Override
+ public void positionSaverTick() {
+
+ }
+
+ @Override
+ public void onSleepTimerExpired() {
+ if (countDownLatch.getCount() == 0) {
+ fail();
+ }
+ countDownLatch.countDown();
+ }
+
+ @Override
+ public void onWidgetUpdaterTick() {
+
+ }
+
+ @Override
+ public void onChapterLoaded(Playable media) {
+
+ }
+ });
+ pstm.setSleepTimer(TIME);
+ countDownLatch.await(TIMEOUT, TimeUnit.MILLISECONDS);
+ pstm.shutdown();
+ }
+
+ public void testDisableSleepTimer() throws InterruptedException {
+ final Context c = getInstrumentation().getTargetContext();
+ final long TIME = 1000;
+ final long TIMEOUT = 2 * TIME;
+ final CountDownLatch countDownLatch = new CountDownLatch(1);
+ PlaybackServiceTaskManager pstm = new PlaybackServiceTaskManager(c, new PlaybackServiceTaskManager.PSTMCallback() {
+ @Override
+ public void positionSaverTick() {
+
+ }
+
+ @Override
+ public void onSleepTimerExpired() {
+ fail("Sleeptimer expired");
+ }
+
+ @Override
+ public void onWidgetUpdaterTick() {
+
+ }
+
+ @Override
+ public void onChapterLoaded(Playable media) {
+
+ }
+ });
+ pstm.setSleepTimer(TIME);
+ pstm.disableSleepTimer();
+ assertFalse(countDownLatch.await(TIMEOUT, TimeUnit.MILLISECONDS));
+ pstm.shutdown();
+ }
+
+ public void testIsSleepTimerActivePositive() {
+ final Context c = getInstrumentation().getTargetContext();
+ PlaybackServiceTaskManager pstm = new PlaybackServiceTaskManager(c, defaultPSTM);
+ pstm.setSleepTimer(10000);
+ assertTrue(pstm.isSleepTimerActive());
+ pstm.shutdown();
+ }
+
+ public void testIsSleepTimerActiveNegative() {
+ final Context c = getInstrumentation().getTargetContext();
+ PlaybackServiceTaskManager pstm = new PlaybackServiceTaskManager(c, defaultPSTM);
+ pstm.setSleepTimer(10000);
+ pstm.disableSleepTimer();
+ assertFalse(pstm.isSleepTimerActive());
+ pstm.shutdown();
+ }
+
+ private final PlaybackServiceTaskManager.PSTMCallback defaultPSTM = new PlaybackServiceTaskManager.PSTMCallback() {
+ @Override
+ public void positionSaverTick() {
+
+ }
+
+ @Override
+ public void onSleepTimerExpired() {
+
+ }
+
+ @Override
+ public void onWidgetUpdaterTick() {
+
+ }
+
+ @Override
+ public void onChapterLoaded(Playable media) {
+
+ }
+ };
+}