summaryrefslogtreecommitdiff
path: root/src/de
diff options
context:
space:
mode:
Diffstat (limited to 'src/de')
-rw-r--r--src/de/danoeh/antennapod/activity/AudioplayerActivity.java983
-rw-r--r--src/de/danoeh/antennapod/activity/DownloadActivity.java112
-rw-r--r--src/de/danoeh/antennapod/activity/MainActivity.java2
-rw-r--r--src/de/danoeh/antennapod/activity/MediaplayerActivity.java33
-rw-r--r--src/de/danoeh/antennapod/activity/VideoplayerActivity.java591
-rw-r--r--src/de/danoeh/antennapod/adapter/DefaultFeedItemlistAdapter.java4
-rw-r--r--src/de/danoeh/antennapod/adapter/ExternalEpisodesListAdapter.java3
-rw-r--r--src/de/danoeh/antennapod/adapter/InternalFeedItemlistAdapter.java4
-rw-r--r--src/de/danoeh/antennapod/asynctask/DownloadObserver.java150
-rw-r--r--src/de/danoeh/antennapod/asynctask/FlattrClickWorker.java2
-rw-r--r--src/de/danoeh/antennapod/asynctask/FlattrStatusFetcher.java88
-rw-r--r--src/de/danoeh/antennapod/feed/Feed.java10
-rw-r--r--src/de/danoeh/antennapod/feed/FeedMedia.java14
-rw-r--r--src/de/danoeh/antennapod/fragment/ExternalPlayerFragment.java13
-rw-r--r--src/de/danoeh/antennapod/gpoddernet/GpodnetClient.java35
-rw-r--r--src/de/danoeh/antennapod/gpoddernet/GpodnetService.java19
-rw-r--r--src/de/danoeh/antennapod/preferences/UserPreferences.java12
-rw-r--r--src/de/danoeh/antennapod/receiver/MediaButtonReceiver.java2
-rw-r--r--src/de/danoeh/antennapod/receiver/PlayerWidget.java2
-rw-r--r--src/de/danoeh/antennapod/service/PlaybackService.java1736
-rw-r--r--src/de/danoeh/antennapod/service/download/AntennapodHttpClient.java95
-rw-r--r--src/de/danoeh/antennapod/service/download/Downloader.java15
-rw-r--r--src/de/danoeh/antennapod/service/download/HttpDownloader.java55
-rw-r--r--src/de/danoeh/antennapod/service/playback/PlaybackService.java1034
-rw-r--r--src/de/danoeh/antennapod/service/playback/PlaybackServiceMediaPlayer.java926
-rw-r--r--src/de/danoeh/antennapod/service/playback/PlaybackServiceTaskManager.java385
-rw-r--r--src/de/danoeh/antennapod/service/playback/PlayerStatus.java (renamed from src/de/danoeh/antennapod/service/PlayerStatus.java)6
-rw-r--r--src/de/danoeh/antennapod/service/playback/PlayerWidgetService.java (renamed from src/de/danoeh/antennapod/service/PlayerWidgetService.java)25
-rw-r--r--src/de/danoeh/antennapod/storage/DBReader.java7
-rw-r--r--src/de/danoeh/antennapod/storage/DBTasks.java4
-rw-r--r--src/de/danoeh/antennapod/storage/DBWriter.java133
-rw-r--r--src/de/danoeh/antennapod/storage/PodDBAdapter.java9
-rw-r--r--src/de/danoeh/antennapod/util/menuhandler/FeedItemMenuHandler.java298
-rw-r--r--src/de/danoeh/antennapod/util/playback/AudioPlayer.java5
-rw-r--r--src/de/danoeh/antennapod/util/playback/IPlayer.java2
-rw-r--r--src/de/danoeh/antennapod/util/playback/PlaybackController.java150
-rw-r--r--src/de/danoeh/antennapod/util/playback/VideoPlayer.java5
-rw-r--r--src/de/danoeh/antennapod/view/AspectRatioVideoView.java97
38 files changed, 4001 insertions, 3065 deletions
diff --git a/src/de/danoeh/antennapod/activity/AudioplayerActivity.java b/src/de/danoeh/antennapod/activity/AudioplayerActivity.java
index db4373036..a55f8120e 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,525 @@ 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;
-
- 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 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 TextView txtvTitle;
- private TextView txtvFeed;
- private Button butPlaybackSpeed;
- private ImageButton butNavLeft;
- private ImageButton butNavRight;
-
- 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];
- }
-
- @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];
- }
-
- 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;
- }
-
- @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 onPause() {
- savePreferences();
- resetFragmentView();
- super.onPause();
- }
-
- @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, "");
-
- 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;
- }
-
- @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 onAwaitingVideoSurface() {
- 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 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() {
-
- @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();
- }
- }
- }
- }
-
- 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(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);
-
- 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() {
+ 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";
+
+ private Fragment[] detachedFragments;
+
+ 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 TextView txtvTitle;
+ private TextView txtvFeed;
+ private Button butPlaybackSpeed;
+ private ImageButton butNavLeft;
+ private ImageButton butNavRight;
+
+ 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];
+ }
+
+ @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];
+ }
+
+ 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;
+ }
+
+ @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 onPause() {
+ savePreferences();
+ resetFragmentView();
+ super.onPause();
+ }
+
+ @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, "");
+
+ 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;
+ }
+
+ @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 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 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() {
+
+ @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();
+ }
+ }
+ }
+ }
+
+ private void updateNavButtonDrawable() {
+
+ final int[] buttonTexts = new int[] {R.string.show_shownotes_label,
+ R.string.show_chapters_label, R.string.show_cover_label};
+
+ final 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));
+ butNavLeft.setContentDescription(getString(buttonTexts[0]));
+
+ butNavRight.setImageDrawable(drawables.getDrawable(1));
+ butNavRight.setContentDescription(getString(buttonTexts[1]));
+
+ break;
+ case POS_DESCR:
+ butNavLeft.setScaleType(ScaleType.CENTER_CROP);
+ butNavLeft.post(new Runnable() {
+
+ @Override
+ public void run() {
+ ImageLoader.getInstance().loadThumbnailBitmap(media,
+ butNavLeft);
+ }
+ });
+ butNavLeft.setContentDescription(getString(buttonTexts[2]));
+
+ butNavRight.setImageDrawable(drawables.getDrawable(1));
+ butNavRight.setContentDescription(getString(buttonTexts[1]));
+ break;
+ case POS_CHAPTERS:
+ butNavLeft.setScaleType(ScaleType.CENTER_CROP);
+ butNavLeft.post(new Runnable() {
+
+ @Override
+ public void run() {
+ ImageLoader.getInstance().loadThumbnailBitmap(media,
+ butNavLeft);
+ }
+ });
+ butNavLeft.setContentDescription(getString(buttonTexts[2]));
+
+ butNavRight.setImageDrawable(drawables.getDrawable(0));
+ butNavRight.setContentDescription(getString(buttonTexts[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);
+
+ 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() {
@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];
+ 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";
+ 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));
+ 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() {
+ butPlaybackSpeed.setOnLongClickListener(new OnLongClickListener() {
@Override
public boolean onLongClick(View v) {
VariableSpeedDialog.showDialog(AudioplayerActivity.this);
return true;
}
});
- }
-
- @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());
- }
- }
-
- @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);
- }
-
- }
- if (currentlyShownPosition == -1) {
- if (!restoreFromPreferences()) {
- switchToFragment(POS_COVER);
- }
- }
- if (currentlyShownFragment instanceof AudioplayerContentFragment) {
- ((AudioplayerContentFragment) currentlyShownFragment)
- .onDataSetChanged(media);
- }
- updateButPlaybackSpeed();
- }
-
- public void notifyMediaPositionChanged() {
- if (chapterFragment != null) {
- ArrayAdapter<SimpleChapter> adapter = (ArrayAdapter<SimpleChapter>) 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");
- startActivity(new Intent(this, VideoplayerActivity.class));
-
- }
- }
-
- @Override
- protected void onBufferStart() {
- postStatusMsg(R.string.player_buffering_msg);
- }
-
- @Override
- protected void onBufferEnd() {
- clearStatusMsg();
- }
-
- public interface AudioplayerContentFragment {
- public void onDataSetChanged(Playable media);
- }
-
- @Override
- protected int getContentViewResourceId() {
- return R.layout.audioplayer_activity;
- }
+ }
+
+ @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());
+ }
+ }
+
+ @Override
+ protected void onPositionObserverUpdate() {
+ super.onPositionObserverUpdate();
+ notifyMediaPositionChanged();
+ }
+
+ @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();
+ return true;
+ }
+
+ public void notifyMediaPositionChanged() {
+ if (chapterFragment != null) {
+ ArrayAdapter<SimpleChapter> adapter = (ArrayAdapter<SimpleChapter>) 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();
+ }
+
+ 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/DownloadActivity.java b/src/de/danoeh/antennapod/activity/DownloadActivity.java
index ee5bb502c..f5986baf5 100644
--- a/src/de/danoeh/antennapod/activity/DownloadActivity.java
+++ b/src/de/danoeh/antennapod/activity/DownloadActivity.java
@@ -1,18 +1,9 @@
package de.danoeh.antennapod.activity;
-import android.annotation.SuppressLint;
-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.res.TypedArray;
-import android.os.AsyncTask;
-import android.os.Build;
import android.os.Bundle;
-import android.os.IBinder;
-import android.support.v4.app.NavUtils;
+import android.os.Handler;
import android.support.v4.view.MenuItemCompat;
import android.support.v7.app.ActionBarActivity;
import android.support.v7.view.ActionMode;
@@ -22,17 +13,18 @@ import android.view.MenuItem;
import android.view.View;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemLongClickListener;
-
import android.widget.ListView;
-
import de.danoeh.antennapod.AppConfig;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.adapter.DownloadlistAdapter;
+import de.danoeh.antennapod.asynctask.DownloadObserver;
import de.danoeh.antennapod.preferences.UserPreferences;
import de.danoeh.antennapod.service.download.DownloadRequest;
-import de.danoeh.antennapod.service.download.DownloadService;
+import de.danoeh.antennapod.service.download.Downloader;
import de.danoeh.antennapod.storage.DownloadRequester;
+import java.util.List;
+
/**
* Shows all running downloads in a list. The list objects are DownloadStatus
* objects created by a DownloadObserver.
@@ -49,13 +41,10 @@ public class DownloadActivity extends ActionBarActivity implements
private ActionMode mActionMode;
private DownloadRequest selectedDownload;
- private DownloadService downloadService = null;
- boolean mIsBound;
-
- private AsyncTask<Void, Void, Void> contentRefresher;
-
private ListView listview;
+ private DownloadObserver downloadObserver;
+
@Override
protected void onCreate(Bundle savedInstanceState) {
setTheme(UserPreferences.getTheme());
@@ -68,22 +57,19 @@ public class DownloadActivity extends ActionBarActivity implements
Log.d(TAG, "Creating Activity");
requester = DownloadRequester.getInstance();
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
+ downloadObserver = new DownloadObserver(this, new Handler(), observerCallback);
}
@Override
protected void onPause() {
super.onPause();
- unbindService(mConnection);
- unregisterReceiver(contentChanged);
+ downloadObserver.onPause();
}
@Override
protected void onResume() {
super.onResume();
- registerReceiver(contentChanged, new IntentFilter(
- DownloadService.ACTION_DOWNLOADS_CONTENT_CHANGED));
- bindService(new Intent(this, DownloadService.class), mConnection, 0);
- startContentRefresher();
+ downloadObserver.onResume();
if (dla != null) {
dla.notifyDataSetChanged();
}
@@ -94,72 +80,8 @@ public class DownloadActivity extends ActionBarActivity implements
super.onStop();
if (AppConfig.DEBUG)
Log.d(TAG, "Stopping Activity");
- stopContentRefresher();
- }
-
- private ServiceConnection mConnection = new ServiceConnection() {
- public void onServiceDisconnected(ComponentName className) {
- downloadService = null;
- mIsBound = false;
- Log.i(TAG, "Closed connection with DownloadService.");
- }
-
- public void onServiceConnected(ComponentName name, IBinder service) {
- downloadService = ((DownloadService.LocalBinder) service)
- .getService();
- mIsBound = true;
- if (AppConfig.DEBUG)
- Log.d(TAG, "Connection to service established");
- dla = new DownloadlistAdapter(DownloadActivity.this, 0,
- downloadService.getDownloads());
- listview.setAdapter(dla);
- dla.notifyDataSetChanged();
- }
- };
-
- @SuppressLint("NewApi")
- private void startContentRefresher() {
- if (contentRefresher != null) {
- contentRefresher.cancel(true);
- }
- contentRefresher = new AsyncTask<Void, Void, Void>() {
- private static final int WAITING_INTERVAL = 1000;
-
- @Override
- protected void onProgressUpdate(Void... values) {
- super.onProgressUpdate(values);
- if (dla != null) {
- if (AppConfig.DEBUG)
- Log.d(TAG, "Refreshing content automatically");
- dla.notifyDataSetChanged();
- }
- }
-
- @Override
- protected Void doInBackground(Void... params) {
- while (!isCancelled()) {
- try {
- Thread.sleep(WAITING_INTERVAL);
- publishProgress();
- } catch (InterruptedException e) {
- return null;
- }
- }
- return null;
- }
- };
- if (android.os.Build.VERSION.SDK_INT > android.os.Build.VERSION_CODES.GINGERBREAD_MR1) {
- contentRefresher.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
- } else {
- contentRefresher.execute();
- }
}
- private void stopContentRefresher() {
- if (contentRefresher != null) {
- contentRefresher.cancel(true);
- }
- }
@Override
protected void onPostCreate(Bundle savedInstanceState) {
@@ -247,16 +169,22 @@ public class DownloadActivity extends ActionBarActivity implements
dla.setSelectedItemIndex(DownloadlistAdapter.SELECTION_NONE);
}
- private BroadcastReceiver contentChanged = new BroadcastReceiver() {
+ private DownloadObserver.Callback observerCallback = new DownloadObserver.Callback() {
@Override
- public void onReceive(Context context, Intent intent) {
+ public void onContentChanged() {
if (dla != null) {
- if (AppConfig.DEBUG)
- Log.d(TAG, "Refreshing content");
dla.notifyDataSetChanged();
}
}
+
+ @Override
+ public void onDownloadDataAvailable(List<Downloader> downloaderList) {
+ dla = new DownloadlistAdapter(DownloadActivity.this, 0,
+ downloaderList);
+ listview.setAdapter(dla);
+ dla.notifyDataSetChanged();
+ }
};
}
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 129134a47..4aae2b091 100644
--- a/src/de/danoeh/antennapod/activity/MediaplayerActivity.java
+++ b/src/de/danoeh/antennapod/activity/MediaplayerActivity.java
@@ -24,7 +24,7 @@ import de.danoeh.antennapod.dialog.TimeDialog;
import de.danoeh.antennapod.feed.FeedItem;
import de.danoeh.antennapod.feed.FeedMedia;
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;
@@ -110,8 +110,8 @@ public abstract class MediaplayerActivity extends ActionBarActivity
}
@Override
- public void loadMediaInfo() {
- MediaplayerActivity.this.loadMediaInfo();
+ public boolean loadMediaInfo() {
+ return MediaplayerActivity.this.loadMediaInfo();
}
@Override
@@ -150,9 +150,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");
@@ -351,8 +355,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();
@@ -392,7 +397,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();
@@ -407,7 +412,10 @@ public abstract class MediaplayerActivity extends ActionBarActivity
/ media.getDuration();
sbPosition.setProgress((int) (progress * sbPosition.getMax()));
}
- }
+ return true;
+ } else {
+ return false;
+ }
}
protected void setupGUI() {
@@ -427,9 +435,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;
-
-/** Activity for playing audio files. */
-public class VideoplayerActivity extends MediaplayerActivity implements
- SurfaceHolder.Callback {
- private static final String TAG = "VideoplayerActivity";
-
- /** 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;
-
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- requestWindowFeature(Window.FEATURE_ACTION_BAR_OVERLAY);
- setTheme(UserPreferences.getTheme());
-
- super.onCreate(savedInstanceState);
- }
-
- @Override
- protected void onPause() {
- super.onPause();
- if (videoControlsToggler != null) {
- videoControlsToggler.cancel(true);
- }
- }
-
- @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 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);
-
- setupVideoControlsToggler();
- getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
- WindowManager.LayoutParams.FLAG_FULLSCREEN);
- }
-
- @Override
- protected void onAwaitingVideoSurface() {
- if (videoSurfaceCreated) {
- if (AppConfig.DEBUG)
- Log.d(TAG,
- "Videosurface already created, setting videosurface now");
- controller.setVideoSurface(videoview.getHolder());
- }
- }
-
- @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 clearStatusMsg() {
- 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();
- }
-
- return true;
- } else {
- return false;
- }
- }
- };
-
- @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();
- }
- }
-
- private void toggleVideoControlsVisibility() {
- if (videoControlsShowing) {
- getSupportActionBar().hide();
- hideVideoControls();
- } else {
- getSupportActionBar().show();
- showVideoControls();
- }
- videoControlsShowing = !videoControlsShowing;
- }
-
- /** Hides the videocontrols after a certain period of time. */
- public class VideoControlsHider extends AsyncTask<Void, Void, Void> {
- @Override
- protected void onCancelled() {
- videoControlsToggler = null;
- }
-
- @Override
- protected void onPostExecute(Void result) {
- videoControlsToggler = 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
- protected Void doInBackground(Void... params) {
- try {
- Thread.sleep(WAITING_INTERVALL);
- } catch (InterruptedException e) {
- return null;
- }
- publishProgress();
- return null;
- }
-
- }
-
- @Override
- public void surfaceChanged(SurfaceHolder holder, int format, int width,
- int height) {
- holder.setFixedSize(width, height);
- }
-
- @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");
- }
- }
-
- }
-
- @Override
- public void surfaceDestroyed(SurfaceHolder holder) {
- if (AppConfig.DEBUG)
- Log.d(TAG, "Videosurface was destroyed");
- videoSurfaceCreated = false;
- controller.notifyVideoSurfaceAbandoned();
- }
-
- @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 onStopTrackingTouch(SeekBar seekBar) {
- super.onStopTrackingTouch(seekBar);
- setupVideoControlsToggler();
- }
-
- @Override
- protected void onBufferStart() {
- progressIndicator.setVisibility(View.VISIBLE);
- }
-
- @Override
- protected void onBufferEnd() {
- progressIndicator.setVisibility(View.INVISIBLE);
- }
-
- private void showVideoControls() {
- videoOverlay.setVisibility(View.VISIBLE);
- videoOverlay.startAnimation(AnimationUtils.loadAnimation(this,
- R.anim.fade_in));
- }
-
- private void hideVideoControls() {
- videoOverlay.startAnimation(AnimationUtils.loadAnimation(this,
- R.anim.fade_out));
- videoOverlay.setVisibility(View.GONE);
- }
-
- @Override
- protected int getContentViewResourceId() {
- return R.layout.videoplayer_activity;
- }
+import de.danoeh.antennapod.view.AspectRatioVideoView;
+
+/**
+ * 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;
+
+ private LinearLayout videoOverlay;
+ private AspectRatioVideoView videoview;
+ private ProgressBar progressIndicator;
+
+ @Override
+ protected void chooseTheme() {
+ setTheme(R.style.Theme_AntennaPod_Dark);
+ }
+
+ @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);
+ }
+ 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 boolean loadMediaInfo() {
+ if (!super.loadMediaInfo()) {
+ return false;
+ }
+ Playable media = controller.getMedia();
+ if (media != null) {
+ getSupportActionBar().setSubtitle(media.getEpisodeTitle());
+ getSupportActionBar().setTitle(media.getFeedTitle());
+ return true;
+ }
+
+ return false;
+ }
+
+ @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);
+
+ setupVideoControlsToggler();
+ getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
+ WindowManager.LayoutParams.FLAG_FULLSCREEN);
+ }
+
+ @Override
+ protected void onAwaitingVideoSurface() {
+ if (videoSurfaceCreated) {
+ if (AppConfig.DEBUG)
+ Log.d(TAG,
+ "Videosurface already created, setting videosurface now");
+
+ Pair<Integer, Integer> 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 postStatusMsg(int resId) {
+ if (resId == R.string.player_preparing_msg) {
+ progressIndicator.setVisibility(View.VISIBLE);
+ } else {
+ progressIndicator.setVisibility(View.INVISIBLE);
+ }
+
+ }
+
+ @Override
+ protected void clearStatusMsg() {
+ 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();
+ }
+
+ return true;
+ } else {
+ return false;
+ }
+ }
+ };
+
+ @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();
+ }
+ }
+
+ private void toggleVideoControlsVisibility() {
+ if (videoControlsShowing) {
+ getSupportActionBar().hide();
+ hideVideoControls();
+ } else {
+ getSupportActionBar().show();
+ showVideoControls();
+ }
+ videoControlsShowing = !videoControlsShowing;
+ }
+
+ /**
+ * Hides the videocontrols after a certain period of time.
+ */
+ public class VideoControlsHider extends AsyncTask<Void, Void, Void> {
+ @Override
+ protected void onCancelled() {
+ videoControlsToggler = null;
+ }
+
+ @Override
+ protected void onPostExecute(Void result) {
+ videoControlsToggler = 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
+ protected Void doInBackground(Void... params) {
+ try {
+ Thread.sleep(WAITING_INTERVALL);
+ } catch (InterruptedException e) {
+ return null;
+ }
+ publishProgress();
+ return 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 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
+ public void surfaceDestroyed(SurfaceHolder holder) {
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Videosurface was destroyed");
+ videoSurfaceCreated = false;
+ controller.notifyVideoSurfaceAbandoned();
+ }
+ };
+
+
+ @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
+ public void onStartTrackingTouch(SeekBar seekBar) {
+ super.onStartTrackingTouch(seekBar);
+ if (videoControlsToggler != null) {
+ videoControlsToggler.cancel(true);
+ }
+ }
+
+ @Override
+ public void onStopTrackingTouch(SeekBar seekBar) {
+ super.onStopTrackingTouch(seekBar);
+ setupVideoControlsToggler();
+ }
+
+ @Override
+ protected void onBufferStart() {
+ progressIndicator.setVisibility(View.VISIBLE);
+ }
+
+ @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/adapter/DefaultFeedItemlistAdapter.java b/src/de/danoeh/antennapod/adapter/DefaultFeedItemlistAdapter.java
index 2b49795c3..e384ecffc 100644
--- a/src/de/danoeh/antennapod/adapter/DefaultFeedItemlistAdapter.java
+++ b/src/de/danoeh/antennapod/adapter/DefaultFeedItemlistAdapter.java
@@ -91,10 +91,12 @@ public class DefaultFeedItemlistAdapter extends BaseAdapter {
MediaType mediaType = item.getMedia().getMediaType();
if (mediaType == MediaType.AUDIO) {
holder.type.setImageDrawable(typeDrawables.getDrawable(0));
+ holder.type.setContentDescription(context.getString(R.string.media_type_audio_label));
holder.type.setVisibility(View.VISIBLE);
} else if (mediaType == MediaType.VIDEO) {
holder.type.setImageDrawable(typeDrawables.getDrawable(1));
- holder.type.setVisibility(View.VISIBLE);
+ holder.type.setContentDescription(context.getString(R.string.media_type_video_label));
+ holder.type.setVisibility(View.VISIBLE);
} else {
holder.type.setImageBitmap(null);
holder.type.setVisibility(View.GONE);
diff --git a/src/de/danoeh/antennapod/adapter/ExternalEpisodesListAdapter.java b/src/de/danoeh/antennapod/adapter/ExternalEpisodesListAdapter.java
index b00066eca..aed988b59 100644
--- a/src/de/danoeh/antennapod/adapter/ExternalEpisodesListAdapter.java
+++ b/src/de/danoeh/antennapod/adapter/ExternalEpisodesListAdapter.java
@@ -148,12 +148,14 @@ public class ExternalEpisodesListAdapter extends BaseExpandableListAdapter {
TypedArray drawables = context.obtainStyledAttributes(new int[] {
R.attr.av_download, R.attr.navigation_refresh });
+ final int[] labels = new int[] {R.string.status_downloaded_label, R.string.downloading_label};
holder.lenSize.setVisibility(View.VISIBLE);
if (!media.isDownloaded()) {
if (DownloadRequester.getInstance().isDownloadingFile(media)) {
holder.downloadStatus.setVisibility(View.VISIBLE);
holder.downloadStatus.setImageDrawable(drawables
.getDrawable(1));
+ holder.downloadStatus.setContentDescription(context.getString(labels[1]));
} else {
holder.downloadStatus.setVisibility(View.INVISIBLE);
}
@@ -161,6 +163,7 @@ public class ExternalEpisodesListAdapter extends BaseExpandableListAdapter {
holder.downloadStatus.setVisibility(View.VISIBLE);
holder.downloadStatus
.setImageDrawable(drawables.getDrawable(0));
+ holder.downloadStatus.setContentDescription(context.getString(labels[0]));
}
} else {
holder.downloadStatus.setVisibility(View.INVISIBLE);
diff --git a/src/de/danoeh/antennapod/adapter/InternalFeedItemlistAdapter.java b/src/de/danoeh/antennapod/adapter/InternalFeedItemlistAdapter.java
index b8bec44c8..238ae29c6 100644
--- a/src/de/danoeh/antennapod/adapter/InternalFeedItemlistAdapter.java
+++ b/src/de/danoeh/antennapod/adapter/InternalFeedItemlistAdapter.java
@@ -176,12 +176,16 @@ public class InternalFeedItemlistAdapter extends DefaultFeedItemlistAdapter {
TypedArray typeDrawables = getContext().obtainStyledAttributes(
new int[] { R.attr.type_audio, R.attr.type_video });
+ final int[] labels = new int[] {R.string.media_type_audio_label, R.string.media_type_video_label};
+
MediaType mediaType = item.getMedia().getMediaType();
if (mediaType == MediaType.AUDIO) {
holder.type.setImageDrawable(typeDrawables.getDrawable(0));
+ holder.type.setContentDescription(getContext().getString(labels[0]));
holder.type.setVisibility(View.VISIBLE);
} else if (mediaType == MediaType.VIDEO) {
holder.type.setImageDrawable(typeDrawables.getDrawable(1));
+ holder.type.setContentDescription(getContext().getString(labels[1]));
holder.type.setVisibility(View.VISIBLE);
} else {
holder.type.setImageBitmap(null);
diff --git a/src/de/danoeh/antennapod/asynctask/DownloadObserver.java b/src/de/danoeh/antennapod/asynctask/DownloadObserver.java
new file mode 100644
index 000000000..26e405615
--- /dev/null
+++ b/src/de/danoeh/antennapod/asynctask/DownloadObserver.java
@@ -0,0 +1,150 @@
+package de.danoeh.antennapod.asynctask;
+
+import android.app.Activity;
+import android.content.*;
+import android.os.Handler;
+import android.os.IBinder;
+import android.util.Log;
+import de.danoeh.antennapod.AppConfig;
+import de.danoeh.antennapod.service.download.DownloadService;
+import de.danoeh.antennapod.service.download.Downloader;
+
+import java.util.List;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * Provides access to the DownloadService's list of items that are currently being downloaded.
+ * The DownloadObserver object should be created in the activity's onCreate() method. resume() and pause()
+ * should be called in the activity's onResume() and onPause() methods
+ */
+public class DownloadObserver {
+ private static final String TAG = "DownloadObserver";
+
+ /**
+ * Time period between update notifications.
+ */
+ public static final int WAITING_INTERVAL_MS = 1000;
+
+ private final Activity activity;
+ private final Handler handler;
+ private final Callback callback;
+
+ private DownloadService downloadService = null;
+ private AtomicBoolean mIsBound = new AtomicBoolean(false);
+
+ private Thread refresherThread;
+ private AtomicBoolean refresherThreadRunning = new AtomicBoolean(false);
+
+
+ /**
+ * Creates a new download observer.
+ *
+ * @param activity Used for registering receivers
+ * @param handler All callback methods are executed on this handler. The handler MUST run on the GUI thread.
+ * @param callback Callback methods for posting content updates
+ * @throws java.lang.IllegalArgumentException if one of the arguments is null.
+ */
+ public DownloadObserver(Activity activity, Handler handler, Callback callback) {
+ if (activity == null) throw new IllegalArgumentException("activity = null");
+ if (handler == null) throw new IllegalArgumentException("handler = null");
+ if (callback == null) throw new IllegalArgumentException("callback = null");
+
+ this.activity = activity;
+ this.handler = handler;
+ this.callback = callback;
+ }
+
+ public void onResume() {
+ if (AppConfig.DEBUG) Log.d(TAG, "DownloadObserver resumed");
+ activity.registerReceiver(contentChangedReceiver, new IntentFilter(DownloadService.ACTION_DOWNLOADS_CONTENT_CHANGED));
+ activity.bindService(new Intent(activity, DownloadService.class), mConnection, 0);
+ }
+
+ public void onPause() {
+ if (AppConfig.DEBUG) Log.d(TAG, "DownloadObserver paused");
+ activity.unregisterReceiver(contentChangedReceiver);
+ activity.unbindService(mConnection);
+ stopRefresher();
+ }
+
+ private BroadcastReceiver contentChangedReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ callback.onContentChanged();
+ startRefresher();
+ }
+ };
+
+ public interface Callback {
+ void onContentChanged();
+
+ void onDownloadDataAvailable(List<Downloader> downloaderList);
+ }
+
+ private ServiceConnection mConnection = new ServiceConnection() {
+ public void onServiceDisconnected(ComponentName className) {
+ downloadService = null;
+ mIsBound.set(false);
+ stopRefresher();
+ Log.i(TAG, "Closed connection with DownloadService.");
+ }
+
+ public void onServiceConnected(ComponentName name, IBinder service) {
+ downloadService = ((DownloadService.LocalBinder) service)
+ .getService();
+ mIsBound.set(true);
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Connection to service established");
+ List<Downloader> downloaderList = downloadService.getDownloads();
+ if (downloaderList != null && !downloaderList.isEmpty()) {
+ callback.onDownloadDataAvailable(downloaderList);
+ startRefresher();
+ }
+ }
+ };
+
+ private void stopRefresher() {
+ if (refresherThread != null) {
+ refresherThread.interrupt();
+ }
+ }
+
+ private void startRefresher() {
+ if (refresherThread == null || refresherThread.isInterrupted()) {
+ refresherThread = new Thread(new RefresherThread());
+ refresherThread.start();
+ }
+ }
+
+ private class RefresherThread implements Runnable {
+
+ public void run() {
+ refresherThreadRunning.set(true);
+ while (!Thread.interrupted()) {
+ try {
+ Thread.sleep(WAITING_INTERVAL_MS);
+ } catch (InterruptedException e) {
+ Log.d(TAG, "Refresher thread was interrupted");
+ }
+ if (mIsBound.get()) {
+ postUpdate();
+ }
+ }
+ refresherThreadRunning.set(false);
+ }
+
+ private void postUpdate() {
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ callback.onContentChanged();
+ List<Downloader> downloaderList = downloadService.getDownloads();
+ if (downloaderList == null || downloaderList.isEmpty()) {
+ Thread.currentThread().interrupt();
+ }
+ }
+ });
+ }
+ }
+
+}
diff --git a/src/de/danoeh/antennapod/asynctask/FlattrClickWorker.java b/src/de/danoeh/antennapod/asynctask/FlattrClickWorker.java
index a023c1e51..458b1261b 100644
--- a/src/de/danoeh/antennapod/asynctask/FlattrClickWorker.java
+++ b/src/de/danoeh/antennapod/asynctask/FlattrClickWorker.java
@@ -174,7 +174,7 @@ public class FlattrClickWorker extends AsyncTask<Void, String, Void> {
else if (run_mode == FLATTR_TOAST)
{
Toast.makeText(context.getApplicationContext(),
- notificationTitle + " " + notificationText,
+ notificationText,
Toast.LENGTH_LONG)
.show();
}
diff --git a/src/de/danoeh/antennapod/asynctask/FlattrStatusFetcher.java b/src/de/danoeh/antennapod/asynctask/FlattrStatusFetcher.java
index 00e91c399..4974c6b56 100644
--- a/src/de/danoeh/antennapod/asynctask/FlattrStatusFetcher.java
+++ b/src/de/danoeh/antennapod/asynctask/FlattrStatusFetcher.java
@@ -1,55 +1,47 @@
package de.danoeh.antennapod.asynctask;
-import java.util.List;
-
-import org.shredzone.flattr4j.exception.FlattrException;
-import org.shredzone.flattr4j.model.Flattr;
-
-import android.util.Log;
-import android.annotation.SuppressLint;
import android.content.Context;
-import android.os.AsyncTask;
+import android.util.Log;
import de.danoeh.antennapod.AppConfig;
-import de.danoeh.antennapod.util.flattr.FlattrUtils;
import de.danoeh.antennapod.storage.DBWriter;
+import de.danoeh.antennapod.util.flattr.FlattrUtils;
+import org.shredzone.flattr4j.exception.FlattrException;
+import org.shredzone.flattr4j.model.Flattr;
-/** Fetch list of flattred things and flattr status in database in a background thread. */
-
-public class FlattrStatusFetcher extends AsyncTask<Void, Void, Void> {
- protected static final String TAG = "FlattrStatusFetcher";
- protected Context context;
-
- public FlattrStatusFetcher(Context context) {
- super();
- this.context = context;
- }
-
- @Override
- protected Void doInBackground(Void... params) {
- if (AppConfig.DEBUG) Log.d(TAG, "Starting background work: Retrieving Flattr status");
-
- Thread.currentThread().setPriority(Thread.MIN_PRIORITY);
-
- try {
- List<Flattr> flattredThings = FlattrUtils.retrieveFlattredThings();
- DBWriter.setFlattredStatus(context, flattredThings);
- }
- catch (FlattrException e) {
- Log.d(TAG, "flattrQueue exception retrieving list with flattred items " + e.getMessage());
- }
-
- if (AppConfig.DEBUG) Log.d(TAG, "Finished background work: Retrieved Flattr status");
-
- return null;
- }
-
- @SuppressLint("NewApi")
- public void executeAsync() {
- FlattrUtils.hasToken();
- if (android.os.Build.VERSION.SDK_INT > android.os.Build.VERSION_CODES.GINGERBREAD_MR1) {
- executeOnExecutor(THREAD_POOL_EXECUTOR);
- } else {
- execute();
- }
- }
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+
+/**
+ * Fetch list of flattred things and flattr status in database in a background thread.
+ */
+
+public class FlattrStatusFetcher extends Thread {
+ protected static final String TAG = "FlattrStatusFetcher";
+ protected Context context;
+
+ public FlattrStatusFetcher(Context context) {
+ super();
+ this.context = context;
+ }
+
+ @Override
+ public void run() {
+ if (AppConfig.DEBUG) Log.d(TAG, "Starting background work: Retrieving Flattr status");
+
+ Thread.currentThread().setPriority(Thread.MIN_PRIORITY);
+
+ try {
+ List<Flattr> flattredThings = FlattrUtils.retrieveFlattredThings();
+ DBWriter.setFlattredStatus(context, flattredThings).get();
+ } catch (FlattrException e) {
+ e.printStackTrace();
+ Log.d(TAG, "flattrQueue exception retrieving list with flattred items " + e.getMessage());
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ } catch (ExecutionException e) {
+ e.printStackTrace();
+ }
+
+ if (AppConfig.DEBUG) Log.d(TAG, "Finished background work: Retrieved Flattr status");
+ }
}
diff --git a/src/de/danoeh/antennapod/feed/Feed.java b/src/de/danoeh/antennapod/feed/Feed.java
index b45ae182a..994446f43 100644
--- a/src/de/danoeh/antennapod/feed/Feed.java
+++ b/src/de/danoeh/antennapod/feed/Feed.java
@@ -85,6 +85,16 @@ public class Feed extends FeedFile implements FlattrThing {
}
/**
+ * This constructor is used for test purposes and uses a default flattr status object.
+ */
+ public Feed(long id, Date lastUpdate, String title, String link, String description, String paymentLink,
+ String author, String language, String type, String feedIdentifier, FeedImage image, String fileUrl,
+ String downloadUrl, boolean downloaded) {
+ this(id, lastUpdate, title, link, description, paymentLink, author, language, type, feedIdentifier, image,
+ fileUrl, downloadUrl, downloaded, new FlattrStatus());
+ }
+
+ /**
* This constructor can be used when parsing feed data. Only the 'lastUpdate' and 'items' field are initialized.
*/
public Feed() {
diff --git a/src/de/danoeh/antennapod/feed/FeedMedia.java b/src/de/danoeh/antennapod/feed/FeedMedia.java
index 321e12863..81cae8507 100644
--- a/src/de/danoeh/antennapod/feed/FeedMedia.java
+++ b/src/de/danoeh/antennapod/feed/FeedMedia.java
@@ -12,9 +12,7 @@ import android.os.Parcel;
import android.os.Parcelable;
import android.util.Log;
import de.danoeh.antennapod.PodcastApp;
-import de.danoeh.antennapod.asynctask.FlattrClickWorker;
import de.danoeh.antennapod.preferences.PlaybackPreferences;
-import de.danoeh.antennapod.service.PlaybackService;
import de.danoeh.antennapod.storage.DBReader;
import de.danoeh.antennapod.storage.DBWriter;
import de.danoeh.antennapod.preferences.UserPreferences;
@@ -148,6 +146,10 @@ public class FeedMedia extends FeedFile implements Playable {
return played_duration;
}
+ public void setPlayedDuration(int played_duration) {
+ this.played_duration = played_duration;
+ }
+
public int getPosition() {
return position;
}
@@ -330,14 +332,6 @@ public class FeedMedia extends FeedFile implements Playable {
@Override
public void saveCurrentPosition(SharedPreferences pref, int newPosition) {
setPosition(newPosition);
-
- // Auto flattr
- if (UserPreferences.isAutoFlattr() && item.getPaymentLink() != null && item.getFlattrStatus().getUnflattred() && (played_duration > 0.8*duration)) {
- Log.d(TAG, "saveCurrentPosition: performing auto flattr since played duration " + Integer.toString(played_duration) + " is 80% of file duration " + Integer.toString(duration));
- item.getFlattrStatus().setFlattrQueue();
- DBWriter.setFeedItemFlattrStatus(PodcastApp.getInstance(), item, false);
- }
-
DBWriter.setFeedMediaPlaybackInformation(PodcastApp.getInstance(), this);
}
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/gpoddernet/GpodnetClient.java b/src/de/danoeh/antennapod/gpoddernet/GpodnetClient.java
deleted file mode 100644
index 845a23823..000000000
--- a/src/de/danoeh/antennapod/gpoddernet/GpodnetClient.java
+++ /dev/null
@@ -1,35 +0,0 @@
-package de.danoeh.antennapod.gpoddernet;
-
-import org.apache.http.conn.ClientConnectionManager;
-import org.apache.http.conn.scheme.PlainSocketFactory;
-import org.apache.http.conn.scheme.Scheme;
-import org.apache.http.conn.scheme.SchemeRegistry;
-import org.apache.http.conn.ssl.SSLSocketFactory;
-import org.apache.http.impl.client.DefaultHttpClient;
-import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager;
-import org.apache.http.params.BasicHttpParams;
-
-/**
- * HTTP client for the gpodder.net service.
- */
-public class GpodnetClient extends DefaultHttpClient {
-
- private static SchemeRegistry prepareSchemeRegistry() {
- SchemeRegistry sr = new SchemeRegistry();
-
- Scheme http = new Scheme("http",
- PlainSocketFactory.getSocketFactory(), 80);
- sr.register(http);
- Scheme https = new Scheme("https",
- SSLSocketFactory.getSocketFactory(), 443);
- sr.register(https);
-
- return sr;
- }
-
- @Override
- protected ClientConnectionManager createClientConnectionManager() {
- return new ThreadSafeClientConnManager(new BasicHttpParams(), prepareSchemeRegistry());
- }
-
-}
diff --git a/src/de/danoeh/antennapod/gpoddernet/GpodnetService.java b/src/de/danoeh/antennapod/gpoddernet/GpodnetService.java
index 6e819f570..a0c5b534c 100644
--- a/src/de/danoeh/antennapod/gpoddernet/GpodnetService.java
+++ b/src/de/danoeh/antennapod/gpoddernet/GpodnetService.java
@@ -1,9 +1,8 @@
package de.danoeh.antennapod.gpoddernet;
-import de.danoeh.antennapod.AppConfig;
import de.danoeh.antennapod.gpoddernet.model.*;
import de.danoeh.antennapod.preferences.GpodnetPreferences;
-import de.danoeh.antennapod.preferences.UserPreferences;
+import de.danoeh.antennapod.service.download.AntennapodHttpClient;
import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
@@ -11,15 +10,13 @@ import org.apache.http.HttpStatus;
import org.apache.http.auth.AuthenticationException;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.ClientProtocolException;
+import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.auth.BasicScheme;
-import org.apache.http.params.CoreProtocolPNames;
-import org.apache.http.params.HttpConnectionParams;
-import org.apache.http.params.HttpParams;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
@@ -45,16 +42,10 @@ public class GpodnetService {
public static final String DEFAULT_BASE_HOST = "gpodder.net";
private final String BASE_HOST;
- private static final int TIMEOUT_MILLIS = 20000;
-
- private final GpodnetClient httpClient;
+ private final HttpClient httpClient;
public GpodnetService() {
- httpClient = new GpodnetClient();
- final HttpParams params = httpClient.getParams();
- params.setParameter(CoreProtocolPNames.USER_AGENT, AppConfig.USER_AGENT);
- HttpConnectionParams.setConnectionTimeout(params, TIMEOUT_MILLIS);
- HttpConnectionParams.setSoTimeout(params, TIMEOUT_MILLIS);
+ httpClient = AntennapodHttpClient.getHttpClient();
BASE_HOST = GpodnetPreferences.getHostname();
}
@@ -519,7 +510,7 @@ public class GpodnetService {
new Thread() {
@Override
public void run() {
- httpClient.getConnectionManager().shutdown();
+ AntennapodHttpClient.cleanup();
}
}.start();
}
diff --git a/src/de/danoeh/antennapod/preferences/UserPreferences.java b/src/de/danoeh/antennapod/preferences/UserPreferences.java
index b850944cf..2b4b66362 100644
--- a/src/de/danoeh/antennapod/preferences/UserPreferences.java
+++ b/src/de/danoeh/antennapod/preferences/UserPreferences.java
@@ -51,6 +51,9 @@ public class UserPreferences implements
private static final String PREF_PLAYBACK_SPEED_ARRAY = "prefPlaybackSpeedArray";
public static final String PREF_PAUSE_PLAYBACK_FOR_FOCUS_LOSS = "prefPauseForFocusLoss";
+ // TODO: Make this value configurable
+ private static final double PLAYED_DURATION_AUTOFLATTR_THRESHOLD = 0.8;
+
private static int EPISODE_CACHE_SIZE_UNLIMITED = -1;
private static UserPreferences instance;
@@ -329,7 +332,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) {
@@ -516,4 +521,9 @@ public class UserPreferences implements
instanceAvailable();
return instance.readEpisodeCacheSizeInternal(valueFromPrefs);
}
+
+ public static double getPlayedDurationAutoflattrThreshold() {
+ instanceAvailable();
+ return PLAYED_DURATION_AUTOFLATTR_THRESHOLD;
+ }
}
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<FeedItem> 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<Void, Void, Void> 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<Void, Void, Void>() {
- 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<Playable, Void, Playable> {
- 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<FeedItem> queueRef = DBReader.getQueue(PlaybackService.this);
- queue = queueRef;
- }
- }
-}
diff --git a/src/de/danoeh/antennapod/service/download/AntennapodHttpClient.java b/src/de/danoeh/antennapod/service/download/AntennapodHttpClient.java
new file mode 100644
index 000000000..7e1c9178a
--- /dev/null
+++ b/src/de/danoeh/antennapod/service/download/AntennapodHttpClient.java
@@ -0,0 +1,95 @@
+package de.danoeh.antennapod.service.download;
+
+import android.util.Log;
+import de.danoeh.antennapod.AppConfig;
+import org.apache.http.client.HttpClient;
+import org.apache.http.client.params.HttpClientParams;
+import org.apache.http.conn.ClientConnectionManager;
+import org.apache.http.conn.params.ConnManagerPNames;
+import org.apache.http.conn.scheme.PlainSocketFactory;
+import org.apache.http.conn.scheme.Scheme;
+import org.apache.http.conn.scheme.SchemeRegistry;
+import org.apache.http.conn.ssl.SSLSocketFactory;
+import org.apache.http.impl.client.AbstractHttpClient;
+import org.apache.http.impl.client.DefaultHttpClient;
+import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager;
+import org.apache.http.params.BasicHttpParams;
+import org.apache.http.params.CoreProtocolPNames;
+import org.apache.http.params.HttpConnectionParams;
+import org.apache.http.params.HttpParams;
+
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Provides access to a HttpClient singleton.
+ */
+public class AntennapodHttpClient {
+ private static final String TAG = "AntennapodHttpClient";
+
+ public static final long EXPIRED_CONN_TIMEOUT_SEC = 30;
+
+ public static final int MAX_REDIRECTS = 5;
+ public static final int CONNECTION_TIMEOUT = 30000;
+ public static final int SOCKET_TIMEOUT = 30000;
+
+ public static final int MAX_CONNECTIONS = 6;
+
+
+ private static volatile HttpClient httpClient = null;
+
+ /**
+ * Returns the HttpClient singleton.
+ */
+ public static synchronized HttpClient getHttpClient() {
+ if (httpClient == null) {
+ if (AppConfig.DEBUG) Log.d(TAG, "Creating new instance of HTTP client");
+
+ HttpParams params = new BasicHttpParams();
+ params.setParameter(CoreProtocolPNames.USER_AGENT, AppConfig.USER_AGENT);
+ params.setIntParameter("http.protocol.max-redirects", MAX_REDIRECTS);
+ params.setBooleanParameter("http.protocol.reject-relative-redirect",
+ false);
+ HttpConnectionParams.setSoTimeout(params, SOCKET_TIMEOUT);
+ HttpConnectionParams.setConnectionTimeout(params, CONNECTION_TIMEOUT);
+ HttpClientParams.setRedirecting(params, true);
+
+ httpClient = new DefaultHttpClient(createClientConnectionManager(), params);
+ // Workaround for broken URLs in redirection
+ ((AbstractHttpClient) httpClient)
+ .setRedirectHandler(new APRedirectHandler());
+ }
+ return httpClient;
+ }
+
+ /**
+ * Closes expired connections. This method should be called by the using class once has finished its work with
+ * the HTTP client.
+ */
+ public static synchronized void cleanup() {
+ if (httpClient != null) {
+ httpClient.getConnectionManager().closeExpiredConnections();
+ httpClient.getConnectionManager().closeIdleConnections(EXPIRED_CONN_TIMEOUT_SEC, TimeUnit.SECONDS);
+ }
+ }
+
+
+ private static ClientConnectionManager createClientConnectionManager() {
+ HttpParams params = new BasicHttpParams();
+ params.setIntParameter(ConnManagerPNames.MAX_TOTAL_CONNECTIONS, MAX_CONNECTIONS);
+ return new ThreadSafeClientConnManager(params, prepareSchemeRegistry());
+ }
+
+ private static SchemeRegistry prepareSchemeRegistry() {
+ SchemeRegistry sr = new SchemeRegistry();
+
+ Scheme http = new Scheme("http",
+ PlainSocketFactory.getSocketFactory(), 80);
+ sr.register(http);
+ Scheme https = new Scheme("https",
+ SSLSocketFactory.getSocketFactory(), 443);
+ sr.register(https);
+
+ return sr;
+ }
+
+}
diff --git a/src/de/danoeh/antennapod/service/download/Downloader.java b/src/de/danoeh/antennapod/service/download/Downloader.java
index 84731fe9f..80cc5b3f8 100644
--- a/src/de/danoeh/antennapod/service/download/Downloader.java
+++ b/src/de/danoeh/antennapod/service/download/Downloader.java
@@ -1,5 +1,8 @@
package de.danoeh.antennapod.service.download;
+import android.content.Context;
+import android.net.wifi.WifiManager;
+import de.danoeh.antennapod.PodcastApp;
import de.danoeh.antennapod.R;
import java.util.concurrent.Callable;
@@ -26,7 +29,19 @@ public abstract class Downloader implements Callable<Downloader> {
protected abstract void download();
public final Downloader call() {
+ WifiManager wifiManager = (WifiManager) PodcastApp.getInstance().getSystemService(Context.WIFI_SERVICE);
+ WifiManager.WifiLock wifiLock = null;
+ if (wifiManager != null) {
+ wifiLock = wifiManager.createWifiLock(TAG);
+ wifiLock.acquire();
+ }
+
download();
+
+ if (wifiLock != null) {
+ wifiLock.release();
+ }
+
if (result == null) {
throw new IllegalStateException(
"Downloader hasn't created DownloadStatus object");
diff --git a/src/de/danoeh/antennapod/service/download/HttpDownloader.java b/src/de/danoeh/antennapod/service/download/HttpDownloader.java
index 94cf01188..fc2b3178b 100644
--- a/src/de/danoeh/antennapod/service/download/HttpDownloader.java
+++ b/src/de/danoeh/antennapod/service/download/HttpDownloader.java
@@ -1,24 +1,5 @@
package de.danoeh.antennapod.service.download;
-import java.io.BufferedInputStream;
-import java.io.BufferedOutputStream;
-import java.io.File;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.net.*;
-
-import org.apache.commons.io.IOUtils;
-import org.apache.http.Header;
-import org.apache.http.HttpEntity;
-import org.apache.http.HttpResponse;
-import org.apache.http.client.methods.HttpGet;
-import org.apache.http.client.params.HttpClientParams;
-import org.apache.http.impl.client.AbstractHttpClient;
-import org.apache.http.impl.client.DefaultHttpClient;
-import org.apache.http.params.HttpConnectionParams;
-import org.apache.http.params.HttpParams;
-
import android.net.http.AndroidHttpClient;
import android.util.Log;
import de.danoeh.antennapod.AppConfig;
@@ -26,36 +7,25 @@ import de.danoeh.antennapod.PodcastApp;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.util.DownloadError;
import de.danoeh.antennapod.util.StorageUtils;
+import org.apache.commons.io.IOUtils;
+import org.apache.http.Header;
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpResponse;
+import org.apache.http.client.HttpClient;
+import org.apache.http.client.methods.HttpGet;
+
+import java.io.*;
+import java.net.*;
public class HttpDownloader extends Downloader {
private static final String TAG = "HttpDownloader";
- private static final int MAX_REDIRECTS = 5;
-
private static final int BUFFER_SIZE = 8 * 1024;
- private static final int CONNECTION_TIMEOUT = 30000;
- private static final int SOCKET_TIMEOUT = 30000;
public HttpDownloader(DownloadRequest request) {
super(request);
}
- private DefaultHttpClient createHttpClient() {
- DefaultHttpClient httpClient = new DefaultHttpClient();
- HttpParams params = httpClient.getParams();
- params.setIntParameter("http.protocol.max-redirects", MAX_REDIRECTS);
- params.setBooleanParameter("http.protocol.reject-relative-redirect",
- false);
- HttpConnectionParams.setSoTimeout(params, SOCKET_TIMEOUT);
- HttpConnectionParams.setConnectionTimeout(params, CONNECTION_TIMEOUT);
- HttpClientParams.setRedirecting(params, true);
-
- // Workaround for broken URLs in redirection
- ((AbstractHttpClient) httpClient)
- .setRedirectHandler(new APRedirectHandler());
- return httpClient;
- }
-
private URI getURIFromRequestUrl(String source) {
try {
URL url = new URL(source);
@@ -69,12 +39,11 @@ public class HttpDownloader extends Downloader {
@Override
protected void download() {
- DefaultHttpClient httpClient = null;
+ HttpClient httpClient = AntennapodHttpClient.getHttpClient();
BufferedOutputStream out = null;
InputStream connection = null;
try {
HttpGet httpGet = new HttpGet(getURIFromRequestUrl(request.getSource()));
- httpClient = createHttpClient();
HttpResponse response = httpClient.execute(httpGet);
HttpEntity httpEntity = response.getEntity();
int responseCode = response.getStatusLine().getStatusCode();
@@ -176,9 +145,7 @@ public class HttpDownloader extends Downloader {
onFail(DownloadError.ERROR_CONNECTION_ERROR, request.getSource());
} finally {
IOUtils.closeQuietly(out);
- if (httpClient != null) {
- httpClient.getConnectionManager().shutdown();
- }
+ AntennapodHttpClient.cleanup();
}
}
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..132932c93
--- /dev/null
+++ b/src/de/danoeh/antennapod/service/playback/PlaybackService.java
@@ -0,0 +1,1034 @@
+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.PodcastApp;
+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.flattr.FlattrUtils;
+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;
+ /**
+ * Is true if service has received a valid start command.
+ */
+ public static boolean started = 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;
+ started = 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);
+ } else {
+
+ if (keycode != -1) {
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Received media button event");
+ handleKeycode(keycode);
+ } else {
+ started = true;
+ 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(true, PlaybackServiceTaskManager.POSITION_SAVER_WAITING_INTERVAL);
+ }
+
+ @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(false, 0);
+ 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<FeedItem> 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<Void, Void, Void> 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<Void, Void, Void>() {
+ 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
+ *
+ * @param updatePlayedDuration true if played_duration should be updated. This applies only to FeedMedia objects
+ * @param deltaPlayedDuration value by which played_duration should be increased.
+ */
+ private synchronized void saveCurrentPosition(boolean updatePlayedDuration, int deltaPlayedDuration) {
+ int position = getCurrentPosition();
+ int duration = getDuration();
+ final Playable playable = mediaPlayer.getPSMPInfo().playable;
+ if (position != INVALID_TIME && duration != INVALID_TIME && playable != null) {
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Saving current position to " + position);
+ if (updatePlayedDuration && playable instanceof FeedMedia) {
+ FeedMedia m = (FeedMedia) playable;
+ FeedItem item = m.getItem();
+ m.setPlayedDuration(m.getPlayedDuration() + deltaPlayedDuration);
+ // Auto flattr
+ if (FlattrUtils.hasToken() && UserPreferences.isAutoFlattr() && item.getPaymentLink() != null && item.getFlattrStatus().getUnflattred() &&
+ (m.getPlayedDuration() > UserPreferences.getPlayedDurationAutoflattrThreshold() * duration)) {
+
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "saveCurrentPosition: performing auto flattr since played duration " + Integer.toString(m.getPlayedDuration())
+ + " is " + UserPreferences.getPlayedDurationAutoflattrThreshold() * 100 + "% of file duration " + Integer.toString(duration));
+ item.getFlattrStatus().setFlattrQueue();
+ DBWriter.setFeedItemFlattrStatus(PodcastApp.getInstance(), item, false);
+ }
+ }
+ 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<FeedItem> 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<Integer, Integer> 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..20e2f703b
--- /dev/null
+++ b/src/de/danoeh/antennapod/service/playback/PlaybackServiceMediaPlayer.java
@@ -0,0 +1,926 @@
+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.LinkedBlockingDeque;
+import java.util.concurrent.RejectedExecutionHandler;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+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<Integer, Integer> 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 ThreadPoolExecutor 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 = new ThreadPoolExecutor(1, 1, 5, TimeUnit.MINUTES, new LinkedBlockingDeque<Runnable>(),
+ new RejectedExecutionHandler() {
+ @Override
+ public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
+ if (AppConfig.DEBUG) Log.d(TAG, "Rejected execution of runnable");
+ }
+ });
+
+ 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.
+ * <p/>
+ * States:
+ * During execution of the method, the object will be in the INITIALIZING state. The end state depends on the given parameters.
+ * <p/>
+ * 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.
+ * <p/>
+ * 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.
+ * <p/>
+ * 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.
+ * <p/>
+ * 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.
+ * <p/>
+ * 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.
+ * <p/>
+ * 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.
+ * <p/>
+ * 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<Integer, Integer>(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.
+ * <p/>
+ * 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.
+ * <p/>
+ * 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.
+ * <p/>
+ * 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<Integer, Integer> getVideoSize() {
+ if (!playerLock.tryLock()) {
+ // use cached value if lock can't be aquired
+ return videoSize;
+ }
+ Pair<Integer, Integer> res;
+ if (mediaPlayer == null || playerStatus == PlayerStatus.ERROR || mediaType != MediaType.VIDEO) {
+ res = null;
+ } else {
+ VideoPlayer vp = (VideoPlayer) mediaPlayer;
+ videoSize = new Pair<Integer, Integer>(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).
+ * <p/>
+ * 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..0c1878e18
--- /dev/null
+++ b/src/de/danoeh/antennapod/service/playback/PlaybackServiceTaskManager.java
@@ -0,0 +1,385 @@
+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.
+ * <p/>
+ * 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<List<FeedItem>> 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<List<FeedItem>>() {
+ @Override
+ public List<FeedItem> 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<FeedItem> 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<FeedItem> 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() && sleepTimer.isWaiting;
+ }
+
+ /**
+ * 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");
+ postExecute();
+ if (!Thread.currentThread().isInterrupted()) {
+ callback.onSleepTimerExpired();
+ }
+
+ }
+ } 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
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
index 475af9655..90ad7a9fa 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;
@@ -6,6 +6,7 @@ import android.appwidget.AppWidgetManager;
import android.content.ComponentName;
import android.content.Intent;
import android.content.ServiceConnection;
+import android.os.Build;
import android.os.IBinder;
import android.util.Log;
import android.view.KeyEvent;
@@ -72,9 +73,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 +86,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());
@@ -95,14 +98,18 @@ public class PlayerWidgetService extends Service {
views.setTextViewText(R.id.txtvProgress, progressString);
}
views.setImageViewResource(R.id.butPlay, R.drawable.av_pause_dark);
+ if (Build.VERSION.SDK_INT >= 15) {
+ views.setContentDescription(R.id.butPlay, getString(R.string.pause_label));
+ }
} else {
views.setImageViewResource(R.id.butPlay, R.drawable.av_play_dark);
+ if (Build.VERSION.SDK_INT >= 15) {
+ views.setContentDescription(R.id.butPlay, getString(R.string.play_label));
+ }
}
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 +133,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/DBReader.java b/src/de/danoeh/antennapod/storage/DBReader.java
index 219d8f042..ccbf6646f 100644
--- a/src/de/danoeh/antennapod/storage/DBReader.java
+++ b/src/de/danoeh/antennapod/storage/DBReader.java
@@ -12,6 +12,7 @@ import de.danoeh.antennapod.util.comparator.DownloadStatusComparator;
import de.danoeh.antennapod.util.comparator.FeedItemPubdateComparator;
import de.danoeh.antennapod.util.flattr.FlattrStatus;
import de.danoeh.antennapod.util.flattr.FlattrThing;
+import de.danoeh.antennapod.util.comparator.PlaybackCompletionDateComparator;
import java.util.ArrayList;
import java.util.Collections;
@@ -215,7 +216,7 @@ public final class DBReader {
.getString(PodDBAdapter.IDX_FI_SMALL_ITEM_IDENTIFIER));
item.setFlattrStatus(new FlattrStatus(itemlistCursor
.getLong(PodDBAdapter.IDX_FI_SMALL_FLATTR_STATUS)));
-
+
// extract chapters
boolean hasSimpleChapters = itemlistCursor
.getInt(PodDBAdapter.IDX_FI_SMALL_HAS_CHAPTERS) > 0;
@@ -522,8 +523,9 @@ public final class DBReader {
List<FeedItem> items = extractItemlistFromCursor(adapter, itemCursor);
loadFeedDataOfFeedItemlist(context, items);
itemCursor.close();
-
adapter.close();
+
+ Collections.sort(items, new PlaybackCompletionDateComparator());
return items;
}
@@ -782,7 +784,6 @@ public final class DBReader {
return media;
}
-
/**
* Returns the flattr queue as a List of FlattrThings. The list consists of Feeds and FeedItems.
*
diff --git a/src/de/danoeh/antennapod/storage/DBTasks.java b/src/de/danoeh/antennapod/storage/DBTasks.java
index d60b26fbb..c6a0dad17 100644
--- a/src/de/danoeh/antennapod/storage/DBTasks.java
+++ b/src/de/danoeh/antennapod/storage/DBTasks.java
@@ -10,7 +10,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;
@@ -157,7 +157,7 @@ public final class DBTasks {
new FlattrClickWorker(context, FlattrClickWorker.FLATTR_NOTIFICATION).executeSync(); // flattr pending things
if (AppConfig.DEBUG) Log.d(TAG, "Fetching flattr status.");
- new FlattrStatusFetcher(context).executeAsync();
+ new FlattrStatusFetcher(context).start();
GpodnetSyncService.sendSyncIntent(context);
autodownloadUndownloadedItems(context);
diff --git a/src/de/danoeh/antennapod/storage/DBWriter.java b/src/de/danoeh/antennapod/storage/DBWriter.java
index b8ff35b1a..444e9ea0c 100644
--- a/src/de/danoeh/antennapod/storage/DBWriter.java
+++ b/src/de/danoeh/antennapod/storage/DBWriter.java
@@ -4,7 +4,6 @@ import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.database.Cursor;
-import android.net.Uri;
import android.preference.PreferenceManager;
import android.util.Log;
import de.danoeh.antennapod.AppConfig;
@@ -12,8 +11,8 @@ import de.danoeh.antennapod.asynctask.FlattrClickWorker;
import de.danoeh.antennapod.feed.*;
import de.danoeh.antennapod.preferences.GpodnetPreferences;
import de.danoeh.antennapod.preferences.PlaybackPreferences;
-import de.danoeh.antennapod.service.PlaybackService;
import de.danoeh.antennapod.service.download.DownloadStatus;
+import de.danoeh.antennapod.service.playback.PlaybackService;
import de.danoeh.antennapod.util.QueueAccess;
import de.danoeh.antennapod.util.flattr.FlattrStatus;
import de.danoeh.antennapod.util.flattr.FlattrThing;
@@ -22,10 +21,11 @@ import org.shredzone.flattr4j.model.Flattr;
import java.io.File;
import java.io.UnsupportedEncodingException;
-import java.net.URI;
-import java.net.URISyntaxException;
import java.net.URLEncoder;
-import java.util.*;
+import java.util.Date;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
@@ -213,30 +213,6 @@ public class DBWriter {
}
/**
- * Saves the FlattrStatus of a Feed object in the database.
- *
- * @param startFlattrClickWorker true if FlattrClickWorker should be started after the FlattrStatus has been saved
- */
- private static Future<?> setFeedFlattrStatus(final Context context,
- final Feed feed,
- final boolean startFlattrClickWorker) {
- return dbExec.submit(new Runnable() {
-
- @Override
- public void run() {
- PodDBAdapter adapter = new PodDBAdapter(context);
- adapter.open();
- adapter.setFeedFlattrStatus(feed);
- adapter.close();
- if (startFlattrClickWorker) {
- new FlattrClickWorker(context, FlattrClickWorker.FLATTR_TOAST).executeAsync();
- }
- }
- });
- }
-
-
- /**
* Adds a FeedMedia object to the playback history. A FeedMedia object is in the playback history if
* its playback completion date is set to a non-null value. This method will set the playback completion date to the
* current date regardless of the current value.
@@ -252,6 +228,9 @@ public class DBWriter {
if (AppConfig.DEBUG)
Log.d(TAG, "Adding new item to playback history");
media.setPlaybackCompletionDate(new Date());
+ // reset played_duration to 0 so that it behaves correctly when the episode is played again
+ media.setPlayedDuration(0);
+
PodDBAdapter adapter = new PodDBAdapter(context);
adapter.open();
adapter.setFeedMediaPlaybackCompletionDate(media);
@@ -787,29 +766,6 @@ public class DBWriter {
}
/**
- * Saves the FlattrStatus of a FeedItem object in the database.
- *
- * @param startFlattrClickWorker true if FlattrClickWorker should be started after the FlattrStatus has been saved
- */
- public static Future<?> setFeedItemFlattrStatus(final Context context,
- final FeedItem item,
- final boolean startFlattrClickWorker) {
- return dbExec.submit(new Runnable() {
-
- @Override
- public void run() {
- PodDBAdapter adapter = new PodDBAdapter(context);
- adapter.open();
- adapter.setFeedItemFlattrStatus(item);
- adapter.close();
- if (startFlattrClickWorker) {
- new FlattrClickWorker(context, FlattrClickWorker.FLATTR_TOAST).executeAsync();
- }
- }
- });
- }
-
- /**
* Saves a FeedImage object in the database. This method will save all attributes of the FeedImage object. The
* contents of FeedComponent-attributes (e.g. the FeedImages's 'feed'-attribute) will not be saved.
*
@@ -878,16 +834,59 @@ public class DBWriter {
}
/**
+ * Saves the FlattrStatus of a FeedItem object in the database.
+ *
+ * @param startFlattrClickWorker true if FlattrClickWorker should be started after the FlattrStatus has been saved
+ */
+ public static Future<?> setFeedItemFlattrStatus(final Context context,
+ final FeedItem item,
+ final boolean startFlattrClickWorker) {
+ return dbExec.submit(new Runnable() {
+
+ @Override
+ public void run() {
+ PodDBAdapter adapter = new PodDBAdapter(context);
+ adapter.open();
+ adapter.setFeedItemFlattrStatus(item);
+ adapter.close();
+ if (startFlattrClickWorker) {
+ new FlattrClickWorker(context, FlattrClickWorker.FLATTR_TOAST).executeAsync();
+ }
+ }
+ });
+ }
+
+ /**
+ * Saves the FlattrStatus of a Feed object in the database.
+ *
+ * @param startFlattrClickWorker true if FlattrClickWorker should be started after the FlattrStatus has been saved
+ */
+ private static Future<?> setFeedFlattrStatus(final Context context,
+ final Feed feed,
+ final boolean startFlattrClickWorker) {
+ return dbExec.submit(new Runnable() {
+
+ @Override
+ public void run() {
+ PodDBAdapter adapter = new PodDBAdapter(context);
+ adapter.open();
+ adapter.setFeedFlattrStatus(feed);
+ adapter.close();
+ if (startFlattrClickWorker) {
+ new FlattrClickWorker(context, FlattrClickWorker.FLATTR_TOAST).executeAsync();
+ }
+ }
+ });
+ }
+
+ /**
* format an url for querying the database
* (postfix a / and apply percent-encoding)
*/
private static String formatURIForQuery(String uri) {
- try
- {
- return URLEncoder.encode(uri.endsWith("/") ? uri.substring(0, uri.length()-1) : uri, "UTF-8");
- }
- catch (UnsupportedEncodingException e)
- {
+ try {
+ return URLEncoder.encode(uri.endsWith("/") ? uri.substring(0, uri.length() - 1) : uri, "UTF-8");
+ } catch (UnsupportedEncodingException e) {
Log.e(TAG, e.getMessage());
return "";
}
@@ -919,8 +918,7 @@ public class DBWriter {
/**
* Reset flattr status to unflattrd for all items
*/
- public static Future<?> clearAllFlattrStatus(final Context context)
- {
+ public static Future<?> clearAllFlattrStatus(final Context context) {
Log.d(TAG, "clearAllFlattrStatus()");
return dbExec.submit(new Runnable() {
@Override
@@ -946,14 +944,13 @@ public class DBWriter {
return dbExec.submit(new Runnable() {
@Override
public void run() {
- PodDBAdapter adapter = new PodDBAdapter(context);
- adapter.open();
- for (Flattr flattr : flattrList) {
- adapter.setItemFlattrStatus(formatURIForQuery(flattr.getThing().getUrl()), new FlattrStatus(flattr.getCreated().getTime()));
+ PodDBAdapter adapter = new PodDBAdapter(context);
+ adapter.open();
+ for (Flattr flattr : flattrList) {
+ adapter.setItemFlattrStatus(formatURIForQuery(flattr.getThing().getUrl()), new FlattrStatus(flattr.getCreated().getTime()));
+ }
+ adapter.close();
}
- adapter.close();
- }
- });
+ });
}
-
}
diff --git a/src/de/danoeh/antennapod/storage/PodDBAdapter.java b/src/de/danoeh/antennapod/storage/PodDBAdapter.java
index ef76349d1..738ccbf50 100644
--- a/src/de/danoeh/antennapod/storage/PodDBAdapter.java
+++ b/src/de/danoeh/antennapod/storage/PodDBAdapter.java
@@ -464,6 +464,7 @@ public class PodDBAdapter {
if (media.getId() != 0) {
ContentValues values = new ContentValues();
values.put(KEY_PLAYBACK_COMPLETION_DATE, media.getPlaybackCompletionDate().getTime());
+ values.put(KEY_PLAYED_DURATION, media.getPlayedDuration());
db.update(TABLE_NAME_FEED_MEDIA, values, KEY_ID + "=?",
new String[]{String.valueOf(media.getId())});
} else {
@@ -837,7 +838,7 @@ public class PodDBAdapter {
*/
public final Cursor getAllFeedsCursor() {
Cursor c = db.query(TABLE_NAME_FEEDS, FEED_SEL_STD, null, null, null, null,
- KEY_TITLE + " ASC");
+ KEY_TITLE + " COLLATE NOCASE ASC");
return c;
}
@@ -975,7 +976,7 @@ public class PodDBAdapter {
/**
* Returns a cursor which contains feed media objects with a playback
- * completion date in descending order.
+ * completion date in ascending order.
*
* @param limit The maximum row count of the returned cursor. Must be an
* integer >= 0.
@@ -986,8 +987,8 @@ public class PodDBAdapter {
throw new IllegalArgumentException("Limit must be >= 0");
}
Cursor c = db.query(TABLE_NAME_FEED_MEDIA, null,
- KEY_PLAYBACK_COMPLETION_DATE + " > 0", null, null,
- null, KEY_PLAYBACK_COMPLETION_DATE + " DESC LIMIT " + limit);
+ KEY_PLAYBACK_COMPLETION_DATE + " > 0 LIMIT " + limit, null, null,
+ null, null);
return c;
}
diff --git a/src/de/danoeh/antennapod/util/menuhandler/FeedItemMenuHandler.java b/src/de/danoeh/antennapod/util/menuhandler/FeedItemMenuHandler.java
index b7e0d05c4..510f3bae8 100644
--- a/src/de/danoeh/antennapod/util/menuhandler/FeedItemMenuHandler.java
+++ b/src/de/danoeh/antennapod/util/menuhandler/FeedItemMenuHandler.java
@@ -3,180 +3,170 @@ package de.danoeh.antennapod.util.menuhandler;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
-import android.util.Log;
-
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;
import de.danoeh.antennapod.storage.DownloadRequester;
import de.danoeh.antennapod.util.QueueAccess;
import de.danoeh.antennapod.util.ShareUtils;
-import de.danoeh.antennapod.util.flattr.FlattrStatus;
-
-import java.util.List;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.Future;
-/** Handles interactions with the FeedItemMenu. */
+/**
+ * Handles interactions with the FeedItemMenu.
+ */
public class FeedItemMenuHandler {
private static final String TAG = "FeedItemMenuHandler";
- private FeedItemMenuHandler() {
-
- }
-
- /**
- * Used by the MenuHandler to access different types of menus through one
- * interface
- */
- public interface MenuInterface {
- /**
- * Implementations of this method should call findItem(id) on their
- * menu-object and call setVisibility(visibility) on the returned
- * MenuItem object.
- */
- abstract void setItemVisibility(int id, boolean visible);
- }
-
- /**
- * This method should be called in the prepare-methods of menus. It changes
- * the visibility of the menu items depending on a FeedItem's attributes.
- *
- * @param mi
- * An instance of MenuInterface that the method uses to change a
- * MenuItem's visibility
- * @param selectedItem
- * The FeedItem for which the menu is supposed to be prepared
- * @param showExtendedMenu
- * True if MenuItems that let the user share information about
- * the FeedItem and visit its website should be set visible. This
- * parameter should be set to false if the menu space is limited.
- * @param queueAccess
- * Used for testing if the queue contains the selected item
- * @return Returns true if selectedItem is not null.
- * */
- public static boolean onPrepareMenu(MenuInterface mi,
- FeedItem selectedItem, boolean showExtendedMenu, QueueAccess queueAccess) {
+ private FeedItemMenuHandler() {
+
+ }
+
+ /**
+ * Used by the MenuHandler to access different types of menus through one
+ * interface
+ */
+ public interface MenuInterface {
+ /**
+ * Implementations of this method should call findItem(id) on their
+ * menu-object and call setVisibility(visibility) on the returned
+ * MenuItem object.
+ */
+ abstract void setItemVisibility(int id, boolean visible);
+ }
+
+ /**
+ * This method should be called in the prepare-methods of menus. It changes
+ * the visibility of the menu items depending on a FeedItem's attributes.
+ *
+ * @param mi An instance of MenuInterface that the method uses to change a
+ * MenuItem's visibility
+ * @param selectedItem The FeedItem for which the menu is supposed to be prepared
+ * @param showExtendedMenu True if MenuItems that let the user share information about
+ * the FeedItem and visit its website should be set visible. This
+ * parameter should be set to false if the menu space is limited.
+ * @param queueAccess Used for testing if the queue contains the selected item
+ * @return Returns true if selectedItem is not null.
+ */
+ public static boolean onPrepareMenu(MenuInterface mi,
+ FeedItem selectedItem, boolean showExtendedMenu, QueueAccess queueAccess) {
if (selectedItem == null) {
return false;
}
- DownloadRequester requester = DownloadRequester.getInstance();
- boolean hasMedia = selectedItem.getMedia() != null;
- boolean downloaded = hasMedia && selectedItem.getMedia().isDownloaded();
- boolean downloading = hasMedia
- && requester.isDownloadingFile(selectedItem.getMedia());
- boolean notLoadedAndNotLoading = hasMedia && (!downloaded)
- && (!downloading);
- boolean isPlaying = hasMedia
- && selectedItem.getState() == FeedItem.State.PLAYING;
-
- FeedItem.State state = selectedItem.getState();
-
- if (!isPlaying) {
- mi.setItemVisibility(R.id.skip_episode_item, false);
- }
- if (!downloaded || isPlaying) {
- mi.setItemVisibility(R.id.play_item, false);
- mi.setItemVisibility(R.id.remove_item, false);
- }
- if (!notLoadedAndNotLoading) {
- mi.setItemVisibility(R.id.download_item, false);
- }
- if (!(notLoadedAndNotLoading | downloading) | isPlaying) {
- mi.setItemVisibility(R.id.stream_item, false);
- }
- if (!downloading) {
- mi.setItemVisibility(R.id.cancel_download_item, false);
- }
-
- boolean isInQueue = queueAccess.contains(selectedItem.getId());
- if (!isInQueue || isPlaying) {
- mi.setItemVisibility(R.id.remove_from_queue_item, false);
- }
- if (!(!isInQueue && selectedItem.getMedia() != null)) {
- mi.setItemVisibility(R.id.add_to_queue_item, false);
- }
- if (!showExtendedMenu || selectedItem.getLink() == null) {
- mi.setItemVisibility(R.id.share_link_item, false);
- }
+ DownloadRequester requester = DownloadRequester.getInstance();
+ boolean hasMedia = selectedItem.getMedia() != null;
+ boolean downloaded = hasMedia && selectedItem.getMedia().isDownloaded();
+ boolean downloading = hasMedia
+ && requester.isDownloadingFile(selectedItem.getMedia());
+ boolean notLoadedAndNotLoading = hasMedia && (!downloaded)
+ && (!downloading);
+ boolean isPlaying = hasMedia
+ && selectedItem.getState() == FeedItem.State.PLAYING;
+
+ FeedItem.State state = selectedItem.getState();
+
+ if (!isPlaying) {
+ mi.setItemVisibility(R.id.skip_episode_item, false);
+ }
+ if (!downloaded || isPlaying) {
+ mi.setItemVisibility(R.id.play_item, false);
+ mi.setItemVisibility(R.id.remove_item, false);
+ }
+ if (!notLoadedAndNotLoading) {
+ mi.setItemVisibility(R.id.download_item, false);
+ }
+ if (!(notLoadedAndNotLoading | downloading) | isPlaying) {
+ mi.setItemVisibility(R.id.stream_item, false);
+ }
+ if (!downloading) {
+ mi.setItemVisibility(R.id.cancel_download_item, false);
+ }
- if (!AppConfig.DEBUG
- || !(state == FeedItem.State.IN_PROGRESS || state == FeedItem.State.READ)) {
- mi.setItemVisibility(R.id.mark_unread_item, false);
- }
- if (!(state == FeedItem.State.NEW || state == FeedItem.State.IN_PROGRESS)) {
- mi.setItemVisibility(R.id.mark_read_item, false);
- }
+ boolean isInQueue = queueAccess.contains(selectedItem.getId());
+ if (!isInQueue || isPlaying) {
+ mi.setItemVisibility(R.id.remove_from_queue_item, false);
+ }
+ if (!(!isInQueue && selectedItem.getMedia() != null)) {
+ mi.setItemVisibility(R.id.add_to_queue_item, false);
+ }
+ if (!showExtendedMenu || selectedItem.getLink() == null) {
+ mi.setItemVisibility(R.id.share_link_item, false);
+ }
- if (!showExtendedMenu || selectedItem.getLink() == null) {
- mi.setItemVisibility(R.id.visit_website_item, false);
- }
+ if (!AppConfig.DEBUG
+ || !(state == FeedItem.State.IN_PROGRESS || state == FeedItem.State.READ)) {
+ mi.setItemVisibility(R.id.mark_unread_item, false);
+ }
+ if (!(state == FeedItem.State.NEW || state == FeedItem.State.IN_PROGRESS)) {
+ mi.setItemVisibility(R.id.mark_read_item, false);
+ }
- if (selectedItem.getPaymentLink() == null || !selectedItem.getFlattrStatus().flattrable()) {
- mi.setItemVisibility(R.id.support_item, false);
- }
- return true;
- }
+ if (!showExtendedMenu || selectedItem.getLink() == null) {
+ mi.setItemVisibility(R.id.visit_website_item, false);
+ }
- public static boolean onMenuItemClicked(Context context, int menuItemId,
- FeedItem selectedItem) throws DownloadRequestException {
- DownloadRequester requester = DownloadRequester.getInstance();
- switch (menuItemId) {
- case R.id.skip_episode_item:
- context.sendBroadcast(new Intent(
- PlaybackService.ACTION_SKIP_CURRENT_EPISODE));
- break;
- case R.id.download_item:
- DBTasks.downloadFeedItems(context, selectedItem);
- break;
- case R.id.play_item:
- DBTasks.playMedia(context, selectedItem.getMedia(), true, true,
- false);
- break;
- case R.id.remove_item:
- DBWriter.deleteFeedMediaOfItem(context, selectedItem.getMedia().getId());
- break;
- case R.id.cancel_download_item:
- requester.cancelDownload(context, selectedItem.getMedia());
- break;
- case R.id.mark_read_item:
- DBWriter.markItemRead(context, selectedItem, true, true);
- break;
- case R.id.mark_unread_item:
- DBWriter.markItemRead(context, selectedItem, false, true);
- break;
- case R.id.add_to_queue_item:
- DBWriter.addQueueItem(context, selectedItem.getId());
- break;
- case R.id.remove_from_queue_item:
- DBWriter.removeQueueItem(context, selectedItem.getId(), true);
- break;
- case R.id.stream_item:
- DBTasks.playMedia(context, selectedItem.getMedia(), true, true,
- true);
- break;
- case R.id.visit_website_item:
- Uri uri = Uri.parse(selectedItem.getLink());
- context.startActivity(new Intent(Intent.ACTION_VIEW, uri));
- break;
- case R.id.support_item:
- selectedItem.getFlattrStatus().setFlattrQueue();
- DBWriter.setFlattredStatus(context, selectedItem, true);
- break;
- case R.id.share_link_item:
- ShareUtils.shareFeedItemLink(context, selectedItem);
- break;
- default:
- return false;
- }
- // Refresh menu state
+ if (selectedItem.getPaymentLink() == null || !selectedItem.getFlattrStatus().flattrable()) {
+ mi.setItemVisibility(R.id.support_item, false);
+ }
+ return true;
+ }
+
+ public static boolean onMenuItemClicked(Context context, int menuItemId,
+ FeedItem selectedItem) throws DownloadRequestException {
+ DownloadRequester requester = DownloadRequester.getInstance();
+ switch (menuItemId) {
+ case R.id.skip_episode_item:
+ context.sendBroadcast(new Intent(
+ PlaybackService.ACTION_SKIP_CURRENT_EPISODE));
+ break;
+ case R.id.download_item:
+ DBTasks.downloadFeedItems(context, selectedItem);
+ break;
+ case R.id.play_item:
+ DBTasks.playMedia(context, selectedItem.getMedia(), true, true,
+ false);
+ break;
+ case R.id.remove_item:
+ DBWriter.deleteFeedMediaOfItem(context, selectedItem.getMedia().getId());
+ break;
+ case R.id.cancel_download_item:
+ requester.cancelDownload(context, selectedItem.getMedia());
+ break;
+ case R.id.mark_read_item:
+ DBWriter.markItemRead(context, selectedItem, true, true);
+ break;
+ case R.id.mark_unread_item:
+ DBWriter.markItemRead(context, selectedItem, false, true);
+ break;
+ case R.id.add_to_queue_item:
+ DBWriter.addQueueItem(context, selectedItem.getId());
+ break;
+ case R.id.remove_from_queue_item:
+ DBWriter.removeQueueItem(context, selectedItem.getId(), true);
+ break;
+ case R.id.stream_item:
+ DBTasks.playMedia(context, selectedItem.getMedia(), true, true,
+ true);
+ break;
+ case R.id.visit_website_item:
+ Uri uri = Uri.parse(selectedItem.getLink());
+ context.startActivity(new Intent(Intent.ACTION_VIEW, uri));
+ break;
+ case R.id.support_item:
+ selectedItem.getFlattrStatus().setFlattrQueue();
+ DBWriter.setFlattredStatus(context, selectedItem, true);
+ break;
+ case R.id.share_link_item:
+ ShareUtils.shareFeedItemLink(context, selectedItem);
+ break;
+ default:
+ return false;
+ }
+ // Refresh menu state
- return true;
- }
+ return true;
+ }
}
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..0781800aa 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);
- }
}
/**
@@ -179,8 +171,9 @@ public abstract class PlaybackController {
@Override
protected void onPostExecute(Intent serviceIntent) {
boolean bound = false;
- if (!PlaybackService.isRunning) {
+ if (!PlaybackService.started) {
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,37 +395,51 @@ 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;
+ final CharSequence playText = activity.getString(R.string.play_label);
+ final CharSequence pauseText = activity.getString(R.string.pause_label);
+
+ 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();
checkMediaInfoLoaded();
cancelPositionObserver();
- updatePlayButtonAppearance(playResource);
+ updatePlayButtonAppearance(playResource, playText);
break;
case PLAYING:
clearStatusMsg();
checkMediaInfoLoaded();
+ if (PlaybackService.getCurrentMediaType() == MediaType.VIDEO) {
+ onAwaitingVideoSurface();
+ }
setupPositionObserver();
- updatePlayButtonAppearance(pauseResource);
+ updatePlayButtonAppearance(pauseResource, pauseText);
break;
case PREPARING:
postStatusMsg(R.string.player_preparing_msg);
checkMediaInfoLoaded();
if (playbackService != null) {
if (playbackService.isStartWhenPrepared()) {
- updatePlayButtonAppearance(pauseResource);
+ updatePlayButtonAppearance(pauseResource, pauseText);
} else {
- updatePlayButtonAppearance(playResource);
+ updatePlayButtonAppearance(playResource, playText);
}
}
break;
@@ -441,32 +449,27 @@ public abstract class PlaybackController {
case PREPARED:
checkMediaInfoLoaded();
postStatusMsg(R.string.player_ready_msg);
- updatePlayButtonAppearance(playResource);
+ updatePlayButtonAppearance(playResource, playText);
break;
case SEEKING:
postStatusMsg(R.string.player_seeking_msg);
break;
- case AWAITING_VIDEO_SURFACE:
- onAwaitingVideoSurface();
- break;
case INITIALIZED:
checkMediaInfoLoaded();
clearStatusMsg();
- updatePlayButtonAppearance(playResource);
+ updatePlayButtonAppearance(playResource, playText);
break;
}
}
private void checkMediaInfoLoaded() {
- if (!mediaInfoLoaded) {
- loadMediaInfo();
- }
- mediaInfoLoaded = true;
+ mediaInfoLoaded = (mediaInfoLoaded || loadMediaInfo());
}
- private void updatePlayButtonAppearance(int resource) {
+ private void updatePlayButtonAppearance(int resource, CharSequence contentDescription) {
ImageButton butPlay = getPlayButton();
butPlay.setImageResource(resource);
+ butPlay.setContentDescription(contentDescription);
}
public abstract ImageButton getPlayButton();
@@ -475,7 +478,7 @@ public abstract class PlaybackController {
public abstract void clearStatusMsg();
- public abstract void loadMediaInfo();
+ public abstract boolean loadMediaInfo();
public abstract void onAwaitingVideoSurface();
@@ -488,7 +491,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 +501,7 @@ public abstract class PlaybackController {
activity.startService(serviceIntent);
}
}
+ */
onServiceQueried();
setupGUI();
@@ -517,7 +522,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 +546,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 +562,7 @@ public abstract class PlaybackController {
break;
case PAUSED:
case PREPARED:
- playbackService.play();
+ playbackService.resume();
break;
case PREPARING:
playbackService.setStartWhenPrepared(!playbackService
@@ -609,7 +614,7 @@ public abstract class PlaybackController {
public int getPosition() {
if (playbackService != null) {
- return playbackService.getCurrentPositionSafe();
+ return playbackService.getCurrentPosition();
} else {
return PlaybackService.INVALID_TIME;
}
@@ -617,7 +622,7 @@ public abstract class PlaybackController {
public int getDuration() {
if (playbackService != null) {
- return playbackService.getDurationSafe();
+ return playbackService.getDuration();
} else {
return PlaybackService.INVALID_TIME;
}
@@ -675,27 +680,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<Integer, Integer> getVideoSize() {
+ if (playbackService != null) {
+ return playbackService.getVideoSize();
+ } else {
+ return null;
+ }
+ }
+
/**
* Returns true if PlaybackController can communicate with the playback
@@ -716,7 +729,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 +746,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();
+ }
+
+}