summaryrefslogtreecommitdiff
path: root/src/de/danoeh
diff options
context:
space:
mode:
authorTom Hennen <tom.hennen@gmail.com>2013-09-09 18:21:00 -0400
committerTom Hennen <tom.hennen@gmail.com>2013-09-09 18:21:00 -0400
commit34a5e62339928d9a6353ba966cf9841e1719b570 (patch)
treee465a9c389323604c48d854603bd21ef89dd800a /src/de/danoeh
parent4e845b83c650b0f103b564da221326b997e8d032 (diff)
parent02926a6e5ffa968d08efeae5012a0ecf41a6f33a (diff)
downloadAntennaPod-34a5e62339928d9a6353ba966cf9841e1719b570.zip
Merge branch 'develop' of https://github.com/danieloeh/AntennaPod into move-to-top
Diffstat (limited to 'src/de/danoeh')
-rw-r--r--src/de/danoeh/antennapod/AppConfig.java2
-rw-r--r--src/de/danoeh/antennapod/activity/AddFeedActivity.java10
-rw-r--r--src/de/danoeh/antennapod/activity/AudioplayerActivity.java70
-rw-r--r--src/de/danoeh/antennapod/activity/DefaultOnlineFeedViewActivity.java164
-rw-r--r--src/de/danoeh/antennapod/activity/DirectoryChooserActivity.java8
-rw-r--r--src/de/danoeh/antennapod/activity/DownloadActivity.java4
-rw-r--r--src/de/danoeh/antennapod/activity/FeedItemlistActivity.java2
-rw-r--r--src/de/danoeh/antennapod/activity/MainActivity.java7
-rw-r--r--src/de/danoeh/antennapod/activity/MediaplayerActivity.java9
-rw-r--r--src/de/danoeh/antennapod/activity/MiroGuideCategoryActivity.java2
-rw-r--r--src/de/danoeh/antennapod/activity/OnlineFeedViewActivity.java430
-rw-r--r--src/de/danoeh/antennapod/activity/OpmlImportFromIntentActivity.java4
-rw-r--r--src/de/danoeh/antennapod/activity/OpmlImportFromPathActivity.java9
-rw-r--r--src/de/danoeh/antennapod/activity/PreferenceActivity.java69
-rw-r--r--src/de/danoeh/antennapod/activity/SearchActivity.java51
-rw-r--r--src/de/danoeh/antennapod/activity/gpoddernet/GpodnetActivity.java44
-rw-r--r--src/de/danoeh/antennapod/activity/gpoddernet/GpodnetAuthenticationActivity.java370
-rw-r--r--src/de/danoeh/antennapod/activity/gpoddernet/GpodnetMainActivity.java89
-rw-r--r--src/de/danoeh/antennapod/activity/gpoddernet/GpodnetSearchActivity.java63
-rw-r--r--src/de/danoeh/antennapod/activity/gpoddernet/GpodnetTagActivity.java64
-rw-r--r--src/de/danoeh/antennapod/adapter/ChapterListAdapter.java47
-rw-r--r--src/de/danoeh/antennapod/adapter/FeedItemlistDescriptionAdapter.java55
-rw-r--r--src/de/danoeh/antennapod/adapter/gpodnet/PodcastListAdapter.java63
-rw-r--r--src/de/danoeh/antennapod/asynctask/BitmapDecodeWorkerTask.java190
-rw-r--r--src/de/danoeh/antennapod/asynctask/ImageDiskCache.java391
-rw-r--r--src/de/danoeh/antennapod/asynctask/ImageLoader.java16
-rw-r--r--src/de/danoeh/antennapod/asynctask/OpmlExportWorker.java16
-rw-r--r--src/de/danoeh/antennapod/asynctask/OpmlFeedQueuer.java3
-rw-r--r--src/de/danoeh/antennapod/asynctask/OpmlImportWorker.java7
-rw-r--r--src/de/danoeh/antennapod/dialog/AuthenticationDialog.java89
-rw-r--r--src/de/danoeh/antennapod/dialog/VariableSpeedDialog.java100
-rw-r--r--src/de/danoeh/antennapod/feed/EventDistributor.java2
-rw-r--r--src/de/danoeh/antennapod/feed/Feed.java12
-rw-r--r--src/de/danoeh/antennapod/feed/FeedItem.java33
-rw-r--r--src/de/danoeh/antennapod/feed/FeedMedia.java18
-rw-r--r--src/de/danoeh/antennapod/fragment/ExternalPlayerFragment.java6
-rw-r--r--src/de/danoeh/antennapod/fragment/FeedlistFragment.java22
-rw-r--r--src/de/danoeh/antennapod/fragment/ItemDescriptionFragment.java16
-rw-r--r--src/de/danoeh/antennapod/fragment/ItemlistFragment.java38
-rw-r--r--src/de/danoeh/antennapod/fragment/gpodnet/PodcastListFragment.java120
-rw-r--r--src/de/danoeh/antennapod/fragment/gpodnet/PodcastTopListFragment.java22
-rw-r--r--src/de/danoeh/antennapod/fragment/gpodnet/SearchListFragment.java48
-rw-r--r--src/de/danoeh/antennapod/fragment/gpodnet/SuggestionListFragment.java26
-rw-r--r--src/de/danoeh/antennapod/fragment/gpodnet/TagListFragment.java96
-rw-r--r--src/de/danoeh/antennapod/gpoddernet/GpodnetClient.java35
-rw-r--r--src/de/danoeh/antennapod/gpoddernet/GpodnetService.java725
-rw-r--r--src/de/danoeh/antennapod/gpoddernet/GpodnetServiceAuthenticationException.java21
-rw-r--r--src/de/danoeh/antennapod/gpoddernet/GpodnetServiceBadStatusCodeException.java12
-rw-r--r--src/de/danoeh/antennapod/gpoddernet/GpodnetServiceException.java19
-rw-r--r--src/de/danoeh/antennapod/gpoddernet/model/GpodnetDevice.java72
-rw-r--r--src/de/danoeh/antennapod/gpoddernet/model/GpodnetPodcast.java64
-rw-r--r--src/de/danoeh/antennapod/gpoddernet/model/GpodnetSubscriptionChange.java40
-rw-r--r--src/de/danoeh/antennapod/gpoddernet/model/GpodnetTag.java46
-rw-r--r--src/de/danoeh/antennapod/gpoddernet/model/GpodnetUploadChangesResponse.java56
-rw-r--r--src/de/danoeh/antennapod/miroguide/conn/MiroGuideConnector.java14
-rw-r--r--src/de/danoeh/antennapod/miroguide/model/MiroGuideItem.java4
-rw-r--r--src/de/danoeh/antennapod/preferences/GpodnetPreferences.java217
-rw-r--r--src/de/danoeh/antennapod/preferences/UserPreferences.java77
-rw-r--r--src/de/danoeh/antennapod/service/GpodnetSyncService.java243
-rw-r--r--src/de/danoeh/antennapod/service/PlaybackService.java386
-rw-r--r--src/de/danoeh/antennapod/service/download/DownloadService.java66
-rw-r--r--src/de/danoeh/antennapod/service/download/DownloadStatus.java7
-rw-r--r--src/de/danoeh/antennapod/service/download/HttpDownloader.java333
-rw-r--r--src/de/danoeh/antennapod/storage/DBReader.java29
-rw-r--r--src/de/danoeh/antennapod/storage/DBTasks.java57
-rw-r--r--src/de/danoeh/antennapod/storage/DBWriter.java30
-rw-r--r--src/de/danoeh/antennapod/storage/DownloadRequester.java8
-rw-r--r--src/de/danoeh/antennapod/storage/FeedItemStatistics.java4
-rw-r--r--src/de/danoeh/antennapod/storage/PodDBAdapter.java38
-rw-r--r--src/de/danoeh/antennapod/syndication/namespace/atom/NSAtom.java2
-rw-r--r--src/de/danoeh/antennapod/syndication/util/SyndDateUtils.java8
-rw-r--r--src/de/danoeh/antennapod/util/ChapterUtils.java8
-rw-r--r--src/de/danoeh/antennapod/util/DuckType.java115
-rw-r--r--src/de/danoeh/antennapod/util/LangUtils.java3
-rw-r--r--src/de/danoeh/antennapod/util/NetworkUtils.java6
-rw-r--r--src/de/danoeh/antennapod/util/URLChecker.java11
-rw-r--r--src/de/danoeh/antennapod/util/comparator/DownloadStatusComparator.java3
-rw-r--r--src/de/danoeh/antennapod/util/comparator/FeedItemPubdateComparator.java2
-rw-r--r--src/de/danoeh/antennapod/util/comparator/PlaybackCompletionDateComparator.java4
-rw-r--r--src/de/danoeh/antennapod/util/menuhandler/FeedItemMenuHandler.java7
-rw-r--r--src/de/danoeh/antennapod/util/menuhandler/FeedMenuHandler.java4
-rw-r--r--src/de/danoeh/antennapod/util/playback/AudioPlayer.java30
-rw-r--r--src/de/danoeh/antennapod/util/playback/ExternalMedia.java8
-rw-r--r--src/de/danoeh/antennapod/util/playback/IPlayer.java64
-rw-r--r--src/de/danoeh/antennapod/util/playback/PlaybackController.java24
-rw-r--r--src/de/danoeh/antennapod/util/playback/VideoPlayer.java62
86 files changed, 5049 insertions, 822 deletions
diff --git a/src/de/danoeh/antennapod/AppConfig.java b/src/de/danoeh/antennapod/AppConfig.java
index 6caea4127..e79eb64e8 100644
--- a/src/de/danoeh/antennapod/AppConfig.java
+++ b/src/de/danoeh/antennapod/AppConfig.java
@@ -3,4 +3,6 @@ package de.danoeh.antennapod;
public final class AppConfig {
/** Should be used for debug logging. */
public final static boolean DEBUG = true;
+ /** Should be used when setting User-Agent header for HTTP-requests. */
+ public final static String USER_AGENT = "AntennaPod/0.9.8.0";
}
diff --git a/src/de/danoeh/antennapod/activity/AddFeedActivity.java b/src/de/danoeh/antennapod/activity/AddFeedActivity.java
index 4085fc8d2..ad1adfa6b 100644
--- a/src/de/danoeh/antennapod/activity/AddFeedActivity.java
+++ b/src/de/danoeh/antennapod/activity/AddFeedActivity.java
@@ -5,6 +5,7 @@ import java.util.Date;
import android.support.v7.app.ActionBarActivity;
import android.view.Menu;
import android.view.MenuItem;
+import de.danoeh.antennapod.activity.gpoddernet.GpodnetMainActivity;
import org.apache.commons.lang3.StringUtils;
import android.app.AlertDialog;
@@ -37,6 +38,7 @@ public class AddFeedActivity extends ActionBarActivity {
private EditText etxtFeedurl;
private Button butBrowseMiroGuide;
+ private Button butBrowserGpoddernet;
private Button butOpmlImport;
private Button butConfirm;
private Button butCancel;
@@ -63,6 +65,7 @@ public class AddFeedActivity extends ActionBarActivity {
}
butBrowseMiroGuide = (Button) findViewById(R.id.butBrowseMiroguide);
+ butBrowserGpoddernet = (Button) findViewById(R.id.butBrowseGpoddernet);
butOpmlImport = (Button) findViewById(R.id.butOpmlImport);
butConfirm = (Button) findViewById(R.id.butConfirm);
butCancel = (Button) findViewById(R.id.butCancel);
@@ -75,6 +78,13 @@ public class AddFeedActivity extends ActionBarActivity {
MiroGuideMainActivity.class));
}
});
+ butBrowserGpoddernet.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ startActivity(new Intent(AddFeedActivity.this,
+ GpodnetMainActivity.class));
+ }
+ });
butOpmlImport.setOnClickListener(new OnClickListener() {
diff --git a/src/de/danoeh/antennapod/activity/AudioplayerActivity.java b/src/de/danoeh/antennapod/activity/AudioplayerActivity.java
index 0a4c8ae14..db4373036 100644
--- a/src/de/danoeh/antennapod/activity/AudioplayerActivity.java
+++ b/src/de/danoeh/antennapod/activity/AudioplayerActivity.java
@@ -12,7 +12,9 @@ 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.widget.ImageView.ScaleType;
import android.widget.ListView;
@@ -22,11 +24,14 @@ import de.danoeh.antennapod.AppConfig;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.adapter.ChapterListAdapter;
import de.danoeh.antennapod.asynctask.ImageLoader;
+import de.danoeh.antennapod.dialog.VariableSpeedDialog;
import de.danoeh.antennapod.feed.Chapter;
import de.danoeh.antennapod.feed.MediaType;
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.util.playback.ExternalMedia;
import de.danoeh.antennapod.util.playback.Playable;
@@ -56,6 +61,7 @@ public class AudioplayerActivity extends MediaplayerActivity {
private TextView txtvTitle;
private TextView txtvFeed;
+ private Button butPlaybackSpeed;
private ImageButton butNavLeft;
private ImageButton butNavRight;
@@ -218,7 +224,7 @@ public class AudioplayerActivity extends MediaplayerActivity {
if (savedPosition != -1) {
switchToFragment(savedPosition);
}
-
+
}
@Override
@@ -363,6 +369,7 @@ public class AudioplayerActivity extends MediaplayerActivity {
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() {
@@ -390,6 +397,65 @@ public class AudioplayerActivity extends MediaplayerActivity {
}
}
});
+
+ butPlaybackSpeed.setOnClickListener(new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ if (controller != null && controller.canSetPlaybackSpeed()) {
+ String[] availableSpeeds = UserPreferences
+ .getPlaybackSpeedArray();
+ String currentSpeed = UserPreferences.getPlaybackSpeed();
+
+ // Provide initial value in case the speed list has changed
+ // out from under us
+ // and our current speed isn't in the new list
+ String newSpeed;
+ if (availableSpeeds.length > 0) {
+ newSpeed = availableSpeeds[0];
+ } else {
+ newSpeed = "1.0";
+ }
+
+ for (int i = 0; i < availableSpeeds.length; i++) {
+ if (availableSpeeds[i].equals(currentSpeed)) {
+ if (i == availableSpeeds.length - 1) {
+ newSpeed = availableSpeeds[0];
+ } else {
+ newSpeed = availableSpeeds[i + 1];
+ }
+ break;
+ }
+ }
+ UserPreferences.setPlaybackSpeed(newSpeed);
+ controller.setPlaybackSpeed(Float.parseFloat(newSpeed));
+ }
+ }
+ });
+
+ butPlaybackSpeed.setOnLongClickListener(new OnLongClickListener() {
+ @Override
+ public boolean onLongClick(View v) {
+ VariableSpeedDialog.showDialog(AudioplayerActivity.this);
+ return true;
+ }
+ });
+ }
+
+ @Override
+ protected void onPlaybackSpeedChange() {
+ super.onPlaybackSpeedChange();
+ updateButPlaybackSpeed();
+ }
+
+ private void updateButPlaybackSpeed() {
+ if (controller == null
+ || (controller.getCurrentPlaybackSpeedMultiplier() == -1)) {
+ butPlaybackSpeed.setVisibility(View.GONE);
+ } else {
+ butPlaybackSpeed.setVisibility(View.VISIBLE);
+ butPlaybackSpeed.setText(UserPreferences.getPlaybackSpeed());
+ }
}
@Override
@@ -421,7 +487,7 @@ public class AudioplayerActivity extends MediaplayerActivity {
((AudioplayerContentFragment) currentlyShownFragment)
.onDataSetChanged(media);
}
-
+ updateButPlaybackSpeed();
}
public void notifyMediaPositionChanged() {
diff --git a/src/de/danoeh/antennapod/activity/DefaultOnlineFeedViewActivity.java b/src/de/danoeh/antennapod/activity/DefaultOnlineFeedViewActivity.java
new file mode 100644
index 000000000..bb56b1d12
--- /dev/null
+++ b/src/de/danoeh/antennapod/activity/DefaultOnlineFeedViewActivity.java
@@ -0,0 +1,164 @@
+package de.danoeh.antennapod.activity;
+
+import android.content.Context;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.support.v4.app.NavUtils;
+import android.view.LayoutInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.Button;
+import android.widget.ImageView;
+import android.widget.ListView;
+import android.widget.TextView;
+import de.danoeh.antennapod.R;
+import de.danoeh.antennapod.adapter.FeedItemlistDescriptionAdapter;
+import de.danoeh.antennapod.asynctask.ImageDiskCache;
+import de.danoeh.antennapod.dialog.DownloadRequestErrorDialogCreator;
+import de.danoeh.antennapod.feed.EventDistributor;
+import de.danoeh.antennapod.feed.Feed;
+import de.danoeh.antennapod.storage.DBReader;
+import de.danoeh.antennapod.storage.DownloadRequestException;
+import de.danoeh.antennapod.storage.DownloadRequester;
+
+import java.util.Date;
+import java.util.List;
+
+/**
+ * Created by daniel on 24.08.13.
+ */
+public class DefaultOnlineFeedViewActivity extends OnlineFeedViewActivity {
+
+ private static final int EVENTS = EventDistributor.DOWNLOAD_HANDLED | EventDistributor.DOWNLOAD_QUEUED | EventDistributor.FEED_LIST_UPDATE;
+ private volatile List<Feed> feeds;
+ private Feed feed;
+
+ private Button subscribeButton;
+
+ @Override
+ protected void onCreate(Bundle arg0) {
+ super.onCreate(arg0);
+ getSupportActionBar().setDisplayHomeAsUpEnabled(true);
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case android.R.id.home:
+ finish();
+ return true;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ @Override
+ protected void loadData() {
+ super.loadData();
+ feeds = DBReader.getFeedList(this);
+ }
+
+ @Override
+ protected void showFeedInformation(final Feed feed) {
+ super.showFeedInformation(feed);
+ setContentView(R.layout.listview_activity);
+
+ this.feed = feed;
+ EventDistributor.getInstance().register(listener);
+ ListView listView = (ListView) findViewById(R.id.listview);
+ LayoutInflater inflater = (LayoutInflater)
+ getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ View header = inflater.inflate(R.layout.onlinefeedview_header, null);
+ listView.addHeaderView(header);
+
+ listView.setAdapter(new FeedItemlistDescriptionAdapter(this, 0, feed.getItems()));
+
+ ImageView cover = (ImageView) header.findViewById(R.id.imgvCover);
+ TextView title = (TextView) header.findViewById(R.id.txtvTitle);
+ TextView author = (TextView) header.findViewById(R.id.txtvAuthor);
+ TextView description = (TextView) header.findViewById(R.id.txtvDescription);
+ subscribeButton = (Button) header.findViewById(R.id.butSubscribe);
+
+ if (feed.getImage() != null) {
+ ImageDiskCache.getDefaultInstance().loadThumbnailBitmap(feed.getImage().getDownload_url(), cover, (int) getResources().getDimension(
+ R.dimen.thumbnail_length));
+ }
+ title.setText(feed.getTitle());
+ author.setText(feed.getAuthor());
+ description.setText(feed.getDescription());
+
+ subscribeButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ try {
+ DownloadRequester.getInstance().downloadFeed(
+ DefaultOnlineFeedViewActivity.this,
+ new Feed(feed.getDownload_url(), new Date(), feed
+ .getTitle()));
+ } catch (DownloadRequestException e) {
+ e.printStackTrace();
+ DownloadRequestErrorDialogCreator.newRequestErrorDialog(DefaultOnlineFeedViewActivity.this,
+ e.getMessage());
+ }
+ setSubscribeButtonState(feed);
+ }
+ });
+ setSubscribeButtonState(feed);
+
+ }
+
+ private boolean feedInFeedlist(Feed feed) {
+ if (feeds == null || feed == null)
+ return false;
+ for (Feed f : feeds) {
+ if (f.getIdentifyingValue().equals(feed.getIdentifyingValue())) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private void setSubscribeButtonState(Feed feed) {
+ if (subscribeButton != null && feed != null) {
+ if (DownloadRequester.getInstance().isDownloadingFile(feed.getDownload_url())) {
+ subscribeButton.setEnabled(false);
+ subscribeButton.setText(R.string.downloading_label);
+ } else if (feedInFeedlist(feed)) {
+ subscribeButton.setEnabled(false);
+ subscribeButton.setText(R.string.subscribed_label);
+ } else {
+ subscribeButton.setEnabled(true);
+ subscribeButton.setText(R.string.subscribe_label);
+ }
+ }
+ }
+
+ EventDistributor.EventListener listener = new EventDistributor.EventListener() {
+ @Override
+ public void update(EventDistributor eventDistributor, Integer arg) {
+ if ((arg & EventDistributor.FEED_LIST_UPDATE) != 0) {
+ new AsyncTask<Void, Void, List<Feed>>() {
+ @Override
+ protected List<Feed> doInBackground(Void... params) {
+ return DBReader.getFeedList(DefaultOnlineFeedViewActivity.this);
+ }
+
+ @Override
+ protected void onPostExecute(List<Feed> feeds) {
+ super.onPostExecute(feeds);
+ DefaultOnlineFeedViewActivity.this.feeds = feeds;
+ setSubscribeButtonState(feed);
+ }
+ }.execute();
+ } else if ((arg & EVENTS) != 0) {
+ setSubscribeButtonState(feed);
+ }
+ }
+ };
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ EventDistributor.getInstance().unregister(listener);
+ }
+}
+
diff --git a/src/de/danoeh/antennapod/activity/DirectoryChooserActivity.java b/src/de/danoeh/antennapod/activity/DirectoryChooserActivity.java
index 984491174..62273c960 100644
--- a/src/de/danoeh/antennapod/activity/DirectoryChooserActivity.java
+++ b/src/de/danoeh/antennapod/activity/DirectoryChooserActivity.java
@@ -347,7 +347,9 @@ public class DirectoryChooserActivity extends ActionBarActivity {
* CREATE_DIRECTORY_NAME.
*/
private int createFolder() {
- if (selectedDir != null && selectedDir.canWrite()) {
+ if (selectedDir == null) {
+ return R.string.create_folder_error;
+ } else if (selectedDir.canWrite()) {
File newDir = new File(selectedDir, CREATE_DIRECTORY_NAME);
if (!newDir.exists()) {
boolean result = newDir.mkdir();
@@ -359,10 +361,8 @@ public class DirectoryChooserActivity extends ActionBarActivity {
} else {
return R.string.create_folder_error_already_exists;
}
- } else if (selectedDir.canWrite() == false) {
- return R.string.create_folder_error_no_write_access;
} else {
- return R.string.create_folder_error;
+ return R.string.create_folder_error_no_write_access;
}
}
diff --git a/src/de/danoeh/antennapod/activity/DownloadActivity.java b/src/de/danoeh/antennapod/activity/DownloadActivity.java
index 40c75d336..57c86f760 100644
--- a/src/de/danoeh/antennapod/activity/DownloadActivity.java
+++ b/src/de/danoeh/antennapod/activity/DownloadActivity.java
@@ -121,7 +121,7 @@ public class DownloadActivity extends ActionBarActivity implements
contentRefresher.cancel(true);
}
contentRefresher = new AsyncTask<Void, Void, Void>() {
- private final int WAITING_INTERVALL = 1000;
+ private static final int WAITING_INTERVAL = 1000;
@Override
protected void onProgressUpdate(Void... values) {
@@ -137,7 +137,7 @@ public class DownloadActivity extends ActionBarActivity implements
protected Void doInBackground(Void... params) {
while (!isCancelled()) {
try {
- Thread.sleep(WAITING_INTERVALL);
+ Thread.sleep(WAITING_INTERVAL);
publishProgress();
} catch (InterruptedException e) {
return null;
diff --git a/src/de/danoeh/antennapod/activity/FeedItemlistActivity.java b/src/de/danoeh/antennapod/activity/FeedItemlistActivity.java
index ee4e39b9d..8fba44e5c 100644
--- a/src/de/danoeh/antennapod/activity/FeedItemlistActivity.java
+++ b/src/de/danoeh/antennapod/activity/FeedItemlistActivity.java
@@ -124,6 +124,8 @@ public class FeedItemlistActivity extends ActionBarActivity {
SearchManager searchManager =
(SearchManager) getSystemService(Context.SEARCH_SERVICE);
SearchView searchView = (SearchView) MenuItemCompat.getActionView(menu.findItem(R.id.search_item));
+
+
searchView.setIconifiedByDefault(true);
searchView.setSearchableInfo(
diff --git a/src/de/danoeh/antennapod/activity/MainActivity.java b/src/de/danoeh/antennapod/activity/MainActivity.java
index 447a436cf..4f25a07f1 100644
--- a/src/de/danoeh/antennapod/activity/MainActivity.java
+++ b/src/de/danoeh/antennapod/activity/MainActivity.java
@@ -183,7 +183,12 @@ public class MainActivity extends ActionBarActivity {
SearchManager searchManager =
(SearchManager) getSystemService(Context.SEARCH_SERVICE);
- SearchView searchView = (SearchView) MenuItemCompat.getActionView(menu.findItem(R.id.search_item));
+ MenuItem searchItem = menu.findItem(R.id.search_item);
+ SearchView searchView = (SearchView) MenuItemCompat.getActionView(searchItem);
+ if (searchView == null) {
+ MenuItemCompat.setActionView(searchItem, new SearchView(this));
+ searchView = (SearchView) MenuItemCompat.getActionView(searchItem);
+ }
searchView.setIconifiedByDefault(true);
SearchableInfo info = searchManager.getSearchableInfo(getComponentName());
diff --git a/src/de/danoeh/antennapod/activity/MediaplayerActivity.java b/src/de/danoeh/antennapod/activity/MediaplayerActivity.java
index 24c92fbbb..af244f2ed 100644
--- a/src/de/danoeh/antennapod/activity/MediaplayerActivity.java
+++ b/src/de/danoeh/antennapod/activity/MediaplayerActivity.java
@@ -129,10 +129,19 @@ public abstract class MediaplayerActivity extends ActionBarActivity
public void onPlaybackEnd() {
finish();
}
+
+ @Override
+ public void onPlaybackSpeedChange() {
+ MediaplayerActivity.this.onPlaybackSpeedChange();
+ }
};
}
+ protected void onPlaybackSpeedChange() {
+
+ }
+
protected void onServiceQueried() {
supportInvalidateOptionsMenu();
}
diff --git a/src/de/danoeh/antennapod/activity/MiroGuideCategoryActivity.java b/src/de/danoeh/antennapod/activity/MiroGuideCategoryActivity.java
index c039e96f8..e3d77a68a 100644
--- a/src/de/danoeh/antennapod/activity/MiroGuideCategoryActivity.java
+++ b/src/de/danoeh/antennapod/activity/MiroGuideCategoryActivity.java
@@ -22,7 +22,7 @@ import de.danoeh.antennapod.preferences.UserPreferences;
public class MiroGuideCategoryActivity extends ActionBarActivity {
private static final String TAG = "MiroGuideCategoryActivity";
- public static String EXTRA_CATEGORY = "category";
+ public static final String EXTRA_CATEGORY = "category";
private ViewPager viewpager;
private CategoryPagerAdapter pagerAdapter;
diff --git a/src/de/danoeh/antennapod/activity/OnlineFeedViewActivity.java b/src/de/danoeh/antennapod/activity/OnlineFeedViewActivity.java
index cb1c66cab..84aa2d26b 100644
--- a/src/de/danoeh/antennapod/activity/OnlineFeedViewActivity.java
+++ b/src/de/danoeh/antennapod/activity/OnlineFeedViewActivity.java
@@ -1,23 +1,15 @@
package de.danoeh.antennapod.activity;
-import java.io.File;
-import java.io.IOException;
-import java.util.Date;
-
-import javax.xml.parsers.ParserConfigurationException;
-
-import android.support.v7.app.ActionBarActivity;
-import org.xml.sax.SAXException;
-
import android.app.AlertDialog;
import android.content.DialogInterface;
import android.content.DialogInterface.OnCancelListener;
import android.os.Bundle;
+import android.support.v7.app.ActionBarActivity;
import android.util.Log;
import android.view.Gravity;
import android.widget.LinearLayout;
import android.widget.ProgressBar;
-
+import android.widget.RelativeLayout;
import de.danoeh.antennapod.AppConfig;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.feed.Feed;
@@ -25,7 +17,6 @@ import de.danoeh.antennapod.preferences.UserPreferences;
import de.danoeh.antennapod.service.download.DownloadRequest;
import de.danoeh.antennapod.service.download.DownloadStatus;
import de.danoeh.antennapod.service.download.Downloader;
-import de.danoeh.antennapod.service.download.DownloaderCallback;
import de.danoeh.antennapod.service.download.HttpDownloader;
import de.danoeh.antennapod.syndication.handler.FeedHandler;
import de.danoeh.antennapod.syndication.handler.UnsupportedFeedtypeException;
@@ -33,207 +24,238 @@ import de.danoeh.antennapod.util.DownloadError;
import de.danoeh.antennapod.util.FileNameGenerator;
import de.danoeh.antennapod.util.StorageUtils;
import de.danoeh.antennapod.util.URLChecker;
+import org.xml.sax.SAXException;
+
+import javax.xml.parsers.ParserConfigurationException;
+import java.io.File;
+import java.io.IOException;
+import java.util.Date;
/**
* Downloads a feed from a feed URL and parses it. Subclasses can display the
* feed object that was parsed. This activity MUST be started with a given URL
* or an Exception will be thrown.
- *
+ * <p/>
* If the feed cannot be downloaded or parsed, an error dialog will be displayed
* and the activity will finish as soon as the error dialog is closed.
*/
public abstract class OnlineFeedViewActivity extends ActionBarActivity {
- private static final String TAG = "OnlineFeedViewActivity";
- private static final String ARG_FEEDURL = "arg.feedurl";
-
- public static final int RESULT_ERROR = 2;
-
- private Feed feed;
- private Downloader downloader;
-
- @Override
- protected void onCreate(Bundle arg0) {
- setTheme(UserPreferences.getTheme());
- super.onCreate(arg0);
- StorageUtils.checkStorageAvailability(this);
- final String feedUrl = getIntent().getStringExtra(ARG_FEEDURL);
- if (feedUrl == null) {
- throw new IllegalArgumentException(
- "Activity must be started with feedurl argument!");
- }
- if (AppConfig.DEBUG)
- Log.d(TAG, "Activity was started with url " + feedUrl);
- setLoadingLayout();
- startFeedDownload(feedUrl);
- }
-
- @Override
- protected void onStop() {
- super.onStop();
- if (downloader != null && !downloader.isFinished()) {
- downloader.cancel();
- }
- }
-
- private DownloaderCallback downloaderCallback = new DownloaderCallback() {
- @Override
- public void onDownloadCompleted(final Downloader downloader) {
- runOnUiThread(new Runnable() {
-
- @Override
- public void run() {
- DownloadStatus status = downloader.getResult();
- if (status != null) {
- if (!status.isCancelled()) {
- if (status.isSuccessful()) {
- parseFeed();
- } else {
- String errorMsg = status.getReason().getErrorString(
- OnlineFeedViewActivity.this);
- if (errorMsg != null
- && status.getReasonDetailed() != null) {
- errorMsg += " ("
- + status.getReasonDetailed() + ")";
- }
- showErrorDialog(errorMsg);
- }
- }
- } else {
- Log.wtf(TAG,
- "DownloadStatus returned by Downloader was null");
- finish();
- }
- }
- });
-
- }
- };
-
- private void startFeedDownload(String url) {
- if (AppConfig.DEBUG)
- Log.d(TAG, "Starting feed download");
- url = URLChecker.prepareURL(url);
- feed = new Feed(url, new Date());
- String fileUrl = new File(getExternalCacheDir(),
- FileNameGenerator.generateFileName(feed.getDownload_url()))
- .toString();
- feed.setFile_url(fileUrl);
- DownloadRequest request = new DownloadRequest(feed.getFile_url(),
- feed.getDownload_url(), "OnlineFeed", 0, Feed.FEEDFILETYPE_FEED);
- /* TODO update
- HttpDownloader httpDownloader = new HttpDownloader(downloaderCallback,
- request);
- httpDownloader.start();
- */
- }
-
- /** Displays a progress indicator. */
- private void setLoadingLayout() {
- LinearLayout ll = new LinearLayout(this);
- LinearLayout.LayoutParams llLayoutParams = new LinearLayout.LayoutParams(
- LinearLayout.LayoutParams.MATCH_PARENT,
- LinearLayout.LayoutParams.MATCH_PARENT);
-
- ProgressBar pb = new ProgressBar(this);
- pb.setIndeterminate(true);
- LinearLayout.LayoutParams pbLayoutParams = new LinearLayout.LayoutParams(
- LinearLayout.LayoutParams.WRAP_CONTENT,
- LinearLayout.LayoutParams.WRAP_CONTENT);
- pbLayoutParams.gravity = Gravity.CENTER;
- ll.addView(pb, pbLayoutParams);
- addContentView(ll, llLayoutParams);
- }
-
- private void parseFeed() {
- if (feed == null || feed.getFile_url() == null) {
- throw new IllegalStateException(
- "feed must be non-null and downloaded when parseFeed is called");
- }
-
- if (AppConfig.DEBUG)
- Log.d(TAG, "Parsing feed");
-
- Thread thread = new Thread() {
-
- @Override
- public void run() {
- String reasonDetailed = new String();
- boolean successful = false;
- FeedHandler handler = new FeedHandler();
- try {
- handler.parseFeed(feed);
- successful = true;
- } catch (SAXException e) {
- e.printStackTrace();
- reasonDetailed = e.getMessage();
- } catch (IOException e) {
- e.printStackTrace();
- reasonDetailed = e.getMessage();
- } catch (ParserConfigurationException e) {
- e.printStackTrace();
- reasonDetailed = e.getMessage();
- } catch (UnsupportedFeedtypeException e) {
- e.printStackTrace();
- reasonDetailed = e.getMessage();
- } finally {
- boolean rc = new File(feed.getFile_url()).delete();
- if (AppConfig.DEBUG)
- Log.d(TAG, "Deleted feed source file. Result: " + rc);
- }
-
- if (successful) {
- runOnUiThread(new Runnable() {
- @Override
- public void run() {
- showFeedInformation();
- }
- });
- } else {
- final String errorMsg =
- DownloadError.ERROR_PARSER_EXCEPTION.getErrorString(
- OnlineFeedViewActivity.this)
- + " (" + reasonDetailed + ")";
- runOnUiThread(new Runnable() {
-
- @Override
- public void run() {
- showErrorDialog(errorMsg);
- }
- });
- }
- }
- };
- thread.start();
- }
-
- /** Called when feed parsed successfully */
- protected void showFeedInformation() {
-
- }
-
- private void showErrorDialog(String errorMsg) {
- AlertDialog.Builder builder = new AlertDialog.Builder(this);
- builder.setTitle(R.string.error_label);
- if (errorMsg != null) {
- builder.setMessage(getString(R.string.error_msg_prefix) + errorMsg);
- } else {
- builder.setMessage(R.string.error_msg_prefix);
- }
- builder.setNeutralButton(android.R.string.ok,
- new DialogInterface.OnClickListener() {
-
- @Override
- public void onClick(DialogInterface dialog, int which) {
- dialog.cancel();
- }
- });
- builder.setOnCancelListener(new OnCancelListener() {
- @Override
- public void onCancel(DialogInterface dialog) {
- setResult(RESULT_ERROR);
- finish();
- }
- });
- }
+ private static final String TAG = "OnlineFeedViewActivity";
+ public static final String ARG_FEEDURL = "arg.feedurl";
+
+ /** Optional argument: specify a title for the actionbar. */
+ public static final String ARG_TITLE = "title";
+
+ public static final int RESULT_ERROR = 2;
+
+ private Feed feed;
+ private Downloader downloader;
+
+ @Override
+ protected void onCreate(Bundle arg0) {
+ setTheme(UserPreferences.getTheme());
+ super.onCreate(arg0);
+
+ if (getIntent() != null && getIntent().hasExtra(ARG_TITLE)) {
+ getSupportActionBar().setTitle(getIntent().getStringExtra(ARG_TITLE));
+ }
+
+ StorageUtils.checkStorageAvailability(this);
+ final String feedUrl = getIntent().getStringExtra(ARG_FEEDURL);
+ if (feedUrl == null) {
+ throw new IllegalArgumentException(
+ "Activity must be started with feedurl argument!");
+ }
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Activity was started with url " + feedUrl);
+ setLoadingLayout();
+ startFeedDownload(feedUrl);
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ if (downloader != null && !downloader.isFinished()) {
+ downloader.cancel();
+ }
+ }
+
+
+ private void onDownloadCompleted(final Downloader downloader) {
+ runOnUiThread(new Runnable() {
+
+ @Override
+ public void run() {
+ if (AppConfig.DEBUG) Log.d(TAG, "Download was completed");
+ DownloadStatus status = downloader.getResult();
+ if (status != null) {
+ if (!status.isCancelled()) {
+ if (status.isSuccessful()) {
+ parseFeed();
+ } else {
+ String errorMsg = status.getReason().getErrorString(
+ OnlineFeedViewActivity.this);
+ if (errorMsg != null
+ && status.getReasonDetailed() != null) {
+ errorMsg += " ("
+ + status.getReasonDetailed() + ")";
+ }
+ showErrorDialog(errorMsg);
+ }
+ }
+ } else {
+ Log.wtf(TAG,
+ "DownloadStatus returned by Downloader was null");
+ finish();
+ }
+ }
+ });
+
+ }
+
+ private void startFeedDownload(String url) {
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Starting feed download");
+ url = URLChecker.prepareURL(url);
+ feed = new Feed(url, new Date());
+ String fileUrl = new File(getExternalCacheDir(),
+ FileNameGenerator.generateFileName(feed.getDownload_url()))
+ .toString();
+ feed.setFile_url(fileUrl);
+ final DownloadRequest request = new DownloadRequest(feed.getFile_url(),
+ feed.getDownload_url(), "OnlineFeed", 0, Feed.FEEDFILETYPE_FEED);
+ downloader = new HttpDownloader(
+ request);
+ new Thread() {
+ @Override
+ public void run() {
+ loadData();
+ downloader.call();
+ onDownloadCompleted(downloader);
+ }
+ }.start();
+
+
+ }
+
+ /**
+ * Displays a progress indicator.
+ */
+ private void setLoadingLayout() {
+ RelativeLayout rl = new RelativeLayout(this);
+ RelativeLayout.LayoutParams rlLayoutParams = new RelativeLayout.LayoutParams(
+ LinearLayout.LayoutParams.MATCH_PARENT,
+ LinearLayout.LayoutParams.MATCH_PARENT);
+
+ ProgressBar pb = new ProgressBar(this);
+ pb.setIndeterminate(true);
+ RelativeLayout.LayoutParams pbLayoutParams = new RelativeLayout.LayoutParams(
+ LinearLayout.LayoutParams.WRAP_CONTENT,
+ LinearLayout.LayoutParams.WRAP_CONTENT);
+ pbLayoutParams.addRule(RelativeLayout.CENTER_IN_PARENT);
+ rl.addView(pb, pbLayoutParams);
+ addContentView(rl, rlLayoutParams);
+ }
+
+ private void parseFeed() {
+ if (feed == null || feed.getFile_url() == null) {
+ throw new IllegalStateException(
+ "feed must be non-null and downloaded when parseFeed is called");
+ }
+
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Parsing feed");
+
+ Thread thread = new Thread() {
+
+ @Override
+ public void run() {
+ String reasonDetailed = "";
+ boolean successful = false;
+ FeedHandler handler = new FeedHandler();
+ try {
+ handler.parseFeed(feed);
+ successful = true;
+ } catch (SAXException e) {
+ e.printStackTrace();
+ reasonDetailed = e.getMessage();
+ } catch (IOException e) {
+ e.printStackTrace();
+ reasonDetailed = e.getMessage();
+ } catch (ParserConfigurationException e) {
+ e.printStackTrace();
+ reasonDetailed = e.getMessage();
+ } catch (UnsupportedFeedtypeException e) {
+ e.printStackTrace();
+ reasonDetailed = e.getMessage();
+ } finally {
+ boolean rc = new File(feed.getFile_url()).delete();
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Deleted feed source file. Result: " + rc);
+ }
+
+ if (successful) {
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ showFeedInformation(feed);
+ }
+ });
+ } else {
+ final String errorMsg =
+ DownloadError.ERROR_PARSER_EXCEPTION.getErrorString(
+ OnlineFeedViewActivity.this)
+ + " (" + reasonDetailed + ")";
+ runOnUiThread(new Runnable() {
+
+ @Override
+ public void run() {
+ showErrorDialog(errorMsg);
+ }
+ });
+ }
+ }
+ };
+ thread.start();
+ }
+
+ /**
+ * Can be used to load data asynchronously.
+ * */
+ protected void loadData() {
+
+ }
+
+ /**
+ * Called when feed parsed successfully
+ */
+ protected void showFeedInformation(Feed feed) {
+
+ }
+
+ private void showErrorDialog(String errorMsg) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ builder.setTitle(R.string.error_label);
+ if (errorMsg != null) {
+ builder.setMessage(getString(R.string.error_msg_prefix) + errorMsg);
+ } else {
+ builder.setMessage(R.string.error_msg_prefix);
+ }
+ builder.setNeutralButton(android.R.string.ok,
+ new DialogInterface.OnClickListener() {
+
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ dialog.cancel();
+ }
+ });
+ builder.setOnCancelListener(new OnCancelListener() {
+ @Override
+ public void onCancel(DialogInterface dialog) {
+ setResult(RESULT_ERROR);
+ finish();
+ }
+ });
+ }
}
diff --git a/src/de/danoeh/antennapod/activity/OpmlImportFromIntentActivity.java b/src/de/danoeh/antennapod/activity/OpmlImportFromIntentActivity.java
index dc698a851..58e3a96dd 100644
--- a/src/de/danoeh/antennapod/activity/OpmlImportFromIntentActivity.java
+++ b/src/de/danoeh/antennapod/activity/OpmlImportFromIntentActivity.java
@@ -7,6 +7,7 @@ import java.net.URL;
import android.app.AlertDialog;
import android.os.Bundle;
import de.danoeh.antennapod.preferences.UserPreferences;
+import de.danoeh.antennapod.util.LangUtils;
/** Lets the user start the OPML-import process. */
public class OpmlImportFromIntentActivity extends OpmlImportBaseActivity {
@@ -20,7 +21,8 @@ public class OpmlImportFromIntentActivity extends OpmlImportBaseActivity {
try {
URL mOpmlURL = new URL(getIntent().getData().toString());
- BufferedReader in = new BufferedReader(new InputStreamReader(mOpmlURL.openStream()));
+ BufferedReader in = new BufferedReader(new InputStreamReader(mOpmlURL.openStream(),
+ LangUtils.UTF_8));
startImport(in);
} catch (Exception e) {
new AlertDialog.Builder(this).setMessage("Cannot open XML - Reason: " + e.getMessage()).show();
diff --git a/src/de/danoeh/antennapod/activity/OpmlImportFromPathActivity.java b/src/de/danoeh/antennapod/activity/OpmlImportFromPathActivity.java
index 259689abf..ece78006f 100644
--- a/src/de/danoeh/antennapod/activity/OpmlImportFromPathActivity.java
+++ b/src/de/danoeh/antennapod/activity/OpmlImportFromPathActivity.java
@@ -1,8 +1,10 @@
package de.danoeh.antennapod.activity;
import java.io.File;
+import java.io.FileInputStream;
import java.io.FileNotFoundException;
-import java.io.FileReader;
+import java.io.InputStreamReader;
+import java.io.IOException;
import java.io.Reader;
import android.app.AlertDialog;
@@ -20,6 +22,7 @@ import android.widget.Toast;
import de.danoeh.antennapod.AppConfig;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.preferences.UserPreferences;
+import de.danoeh.antennapod.util.LangUtils;
import de.danoeh.antennapod.util.StorageUtils;
/**
@@ -125,8 +128,10 @@ public class OpmlImportFromPathActivity extends OpmlImportBaseActivity {
}
private void startImport(File file) {
+ Reader mReader = null;
try {
- Reader mReader = new FileReader(file);
+ mReader = new InputStreamReader(new FileInputStream(file),
+ LangUtils.UTF_8);
if (AppConfig.DEBUG) Log.d(TAG, "Parsing " + file.toString());
startImport(mReader);
} catch (FileNotFoundException e) {
diff --git a/src/de/danoeh/antennapod/activity/PreferenceActivity.java b/src/de/danoeh/antennapod/activity/PreferenceActivity.java
index 880724c28..bae6c2e17 100644
--- a/src/de/danoeh/antennapod/activity/PreferenceActivity.java
+++ b/src/de/danoeh/antennapod/activity/PreferenceActivity.java
@@ -1,10 +1,5 @@
package de.danoeh.antennapod.activity;
-import java.io.File;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
-
import android.content.Context;
import android.content.Intent;
import android.content.res.Resources.Theme;
@@ -18,16 +13,24 @@ import android.preference.Preference.OnPreferenceChangeListener;
import android.preference.Preference.OnPreferenceClickListener;
import android.preference.PreferenceScreen;
import android.util.Log;
-
import android.view.Menu;
import android.view.MenuItem;
+import android.widget.Toast;
import de.danoeh.antennapod.AppConfig;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.asynctask.FlattrClickWorker;
import de.danoeh.antennapod.asynctask.OpmlExportWorker;
+import de.danoeh.antennapod.dialog.AuthenticationDialog;
+import de.danoeh.antennapod.dialog.VariableSpeedDialog;
+import de.danoeh.antennapod.preferences.GpodnetPreferences;
import de.danoeh.antennapod.preferences.UserPreferences;
import de.danoeh.antennapod.util.flattr.FlattrUtils;
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
/**
* The main preference activity
*/
@@ -41,6 +44,11 @@ public class PreferenceActivity extends android.preference.PreferenceActivity {
private static final String PREF_ABOUT = "prefAbout";
private static final String PREF_CHOOSE_DATA_DIR = "prefChooseDataDir";
private static final String AUTO_DL_PREF_SCREEN = "prefAutoDownloadSettings";
+ private static final String PREF_PLAYBACK_SPEED_LAUNCHER = "prefPlaybackSpeedLauncher";
+
+ private static final String PREF_GPODNET_LOGIN = "pref_gpodnet_authenticate";
+ private static final String PREF_GPODNET_SETLOGIN_INFORMATION = "pref_gpodnet_setlogin_information";
+ private static final String PREF_GPODNET_LOGOUT = "pref_gpodnet_logout";
private CheckBoxPreference[] selectedNetworks;
@@ -54,9 +62,9 @@ public class PreferenceActivity extends android.preference.PreferenceActivity {
getActionBar().setDisplayHomeAsUpEnabled(true);
}
- addPreferencesFromResource(R.xml.preferences);
- findPreference(PREF_FLATTR_THIS_APP).setOnPreferenceClickListener(
- new OnPreferenceClickListener() {
+ addPreferencesFromResource(R.xml.preferences);
+ findPreference(PREF_FLATTR_THIS_APP).setOnPreferenceClickListener(
+ new OnPreferenceClickListener() {
@Override
public boolean onPreferenceClick(Preference preference) {
@@ -156,11 +164,53 @@ public class PreferenceActivity extends android.preference.PreferenceActivity {
return true;
}
});
+ findPreference(PREF_PLAYBACK_SPEED_LAUNCHER)
+ .setOnPreferenceClickListener(new OnPreferenceClickListener() {
+ @Override
+ public boolean onPreferenceClick(Preference preference) {
+ VariableSpeedDialog.showDialog(PreferenceActivity.this);
+ return true;
+ }
+ });
+ findPreference(PREF_GPODNET_SETLOGIN_INFORMATION).setOnPreferenceClickListener(new OnPreferenceClickListener() {
+ @Override
+ public boolean onPreferenceClick(Preference preference) {
+ AuthenticationDialog dialog = new AuthenticationDialog(PreferenceActivity.this,
+ R.string.pref_gpodnet_setlogin_information_title, false, false, GpodnetPreferences.getUsername(),
+ null) {
+
+ @Override
+ protected void onConfirmed(String username, String password, boolean saveUsernamePassword) {
+ GpodnetPreferences.setPassword(password);
+ }
+ };
+ dialog.show();
+ return true;
+ }
+ });
+ findPreference(PREF_GPODNET_LOGOUT).setOnPreferenceClickListener(new OnPreferenceClickListener() {
+ @Override
+ public boolean onPreferenceClick(Preference preference) {
+ GpodnetPreferences.logout();
+ Toast toast = Toast.makeText(PreferenceActivity.this, R.string.pref_gpodnet_logout_toast, Toast.LENGTH_SHORT);
+ toast.show();
+ updateGpodnetPreferenceScreen();
+ return true;
+ }
+ });
buildUpdateIntervalPreference();
buildAutodownloadSelectedNetworsPreference();
setSelectedNetworksEnabled(UserPreferences
.isEnableAutodownloadWifiFilter());
+
+ }
+
+ private void updateGpodnetPreferenceScreen() {
+ final boolean loggedIn = GpodnetPreferences.loggedIn();
+ findPreference(PREF_GPODNET_LOGIN).setEnabled(!loggedIn);
+ findPreference(PREF_GPODNET_SETLOGIN_INFORMATION).setEnabled(loggedIn);
+ findPreference(PREF_GPODNET_LOGOUT).setEnabled(loggedIn);
}
private void buildUpdateIntervalPreference() {
@@ -204,6 +254,7 @@ public class PreferenceActivity extends android.preference.PreferenceActivity {
checkItemVisibility();
setEpisodeCacheSizeText(UserPreferences.getEpisodeCacheSize());
setDataFolderText();
+ updateGpodnetPreferenceScreen();
}
@SuppressWarnings("deprecation")
diff --git a/src/de/danoeh/antennapod/activity/SearchActivity.java b/src/de/danoeh/antennapod/activity/SearchActivity.java
index 257ae86ae..86f7301cf 100644
--- a/src/de/danoeh/antennapod/activity/SearchActivity.java
+++ b/src/de/danoeh/antennapod/activity/SearchActivity.java
@@ -4,6 +4,7 @@ import java.util.ArrayList;
import java.util.List;
import android.annotation.SuppressLint;
+import android.app.Activity;
import android.app.SearchManager;
import android.content.Intent;
import android.os.Bundle;
@@ -140,32 +141,34 @@ public class SearchActivity extends ActionBarActivity implements AdapterView.OnI
@Override
public void run() {
Log.d(TAG, "Starting background work");
+ final Activity activity = SearchActivity.this;
final List<SearchResult> result = FeedSearcher
- .performSearch(SearchActivity.this, query, feedID);
- if (SearchActivity.this != null) {
- SearchActivity.this.runOnUiThread(new Runnable() {
-
- @Override
- public void run() {
- if (AppConfig.DEBUG)
- Log.d(TAG, "Background work finished");
- if (AppConfig.DEBUG)
- Log.d(TAG, "Found " + result.size()
- + " results");
-
- searchAdapter.clear();
- searchAdapter.addAll(result);
- searchAdapter.notifyDataSetChanged();
- txtvStatus
- .setText(R.string.search_status_no_results);
- if (!searchAdapter.isEmpty()) {
- txtvStatus.setVisibility(View.GONE);
- } else {
- txtvStatus.setVisibility(View.VISIBLE);
- }
+ .performSearch(activity, query, feedID);
+ activity.runOnUiThread(new Runnable() {
+
+ @Override
+ public void run() {
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Background work finished");
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Found " + result.size()
+ + " results");
+
+ searchAdapter.clear();
+ for (SearchResult s : result) {
+ searchAdapter.add(s);
}
- });
- }
+ searchAdapter.notifyDataSetChanged();
+ txtvStatus
+ .setText(R.string.search_status_no_results);
+ if (!searchAdapter.isEmpty()) {
+ txtvStatus.setVisibility(View.GONE);
+ } else {
+ txtvStatus.setVisibility(View.VISIBLE);
+ }
+ }
+ });
+
}
};
thread.start();
diff --git a/src/de/danoeh/antennapod/activity/gpoddernet/GpodnetActivity.java b/src/de/danoeh/antennapod/activity/gpoddernet/GpodnetActivity.java
new file mode 100644
index 000000000..08b37ae60
--- /dev/null
+++ b/src/de/danoeh/antennapod/activity/gpoddernet/GpodnetActivity.java
@@ -0,0 +1,44 @@
+package de.danoeh.antennapod.activity.gpoddernet;
+
+import android.app.SearchManager;
+import android.content.Context;
+import android.os.Bundle;
+import android.support.v4.view.MenuItemCompat;
+import android.support.v7.app.ActionBarActivity;
+import android.support.v7.widget.SearchView;
+import android.view.Menu;
+import android.view.MenuItem;
+import de.danoeh.antennapod.R;
+import de.danoeh.antennapod.preferences.UserPreferences;
+
+/**
+ * Created by daniel on 23.08.13.
+ */
+public class GpodnetActivity extends ActionBarActivity {
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ setTheme(UserPreferences.getTheme());
+ super.onCreate(savedInstanceState);
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ MenuItemCompat.setShowAsAction(menu.add(Menu.NONE, R.id.search_item, Menu.NONE, R.string.search_label)
+ .setIcon(
+ obtainStyledAttributes(
+ new int[]{R.attr.action_search})
+ .getDrawable(0)),
+ MenuItem.SHOW_AS_ACTION_IF_ROOM);
+ MenuItemCompat.setActionView(menu.findItem(R.id.search_item), new SearchView(this));
+
+ SearchManager searchManager =
+ (SearchManager) getSystemService(Context.SEARCH_SERVICE);
+ SearchView searchView = (SearchView) MenuItemCompat.getActionView(menu.findItem(R.id.search_item));
+ searchView.setIconifiedByDefault(true);
+ searchView.setSearchableInfo(
+ searchManager.getSearchableInfo(getComponentName()));
+
+ return true;
+ }
+}
diff --git a/src/de/danoeh/antennapod/activity/gpoddernet/GpodnetAuthenticationActivity.java b/src/de/danoeh/antennapod/activity/gpoddernet/GpodnetAuthenticationActivity.java
new file mode 100644
index 000000000..d355a7826
--- /dev/null
+++ b/src/de/danoeh/antennapod/activity/gpoddernet/GpodnetAuthenticationActivity.java
@@ -0,0 +1,370 @@
+package de.danoeh.antennapod.activity.gpoddernet;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.Configuration;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.support.v4.app.NavUtils;
+import android.support.v7.app.ActionBarActivity;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.*;
+import de.danoeh.antennapod.AppConfig;
+import de.danoeh.antennapod.R;
+import de.danoeh.antennapod.activity.MainActivity;
+import de.danoeh.antennapod.gpoddernet.GpodnetService;
+import de.danoeh.antennapod.gpoddernet.GpodnetServiceException;
+import de.danoeh.antennapod.gpoddernet.model.GpodnetDevice;
+import de.danoeh.antennapod.preferences.GpodnetPreferences;
+import de.danoeh.antennapod.preferences.UserPreferences;
+import de.danoeh.antennapod.service.GpodnetSyncService;
+
+import java.security.SecureRandom;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * Guides the user through the authentication process
+ * Step 1: Request username and password from user
+ * Step 2: Choose device from a list of available devices or create a new one
+ * Step 3: Choose from a list of actions
+ */
+public class GpodnetAuthenticationActivity extends ActionBarActivity {
+ private static final String TAG = "GpodnetAuthenticationActivity";
+
+ private static final String CURRENT_STEP = "current_step";
+
+ private ViewFlipper viewFlipper;
+
+ private static final int STEP_DEFAULT = -1;
+ private static final int STEP_LOGIN = 0;
+ private static final int STEP_DEVICE = 1;
+ private static final int STEP_FINISH = 2;
+
+ private int currentStep = -1;
+
+ private GpodnetService service;
+ private volatile String username;
+ private volatile String password;
+ private volatile GpodnetDevice selectedDevice;
+
+ View[] views;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ getSupportActionBar().setDisplayHomeAsUpEnabled(true);
+ setTheme(UserPreferences.getTheme());
+
+ setContentView(R.layout.gpodnetauth_activity);
+ service = new GpodnetService();
+
+ viewFlipper = (ViewFlipper) findViewById(R.id.viewflipper);
+ LayoutInflater inflater = (LayoutInflater)
+ getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ views = new View[]{
+ inflater.inflate(R.layout.gpodnetauth_credentials, null),
+ inflater.inflate(R.layout.gpodnetauth_device, null),
+ inflater.inflate(R.layout.gpodnetauth_finish, null)
+ };
+ for (View view : views) {
+ viewFlipper.addView(view);
+ }
+ advance();
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ if (service != null) {
+ service.shutdown();
+ }
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case android.R.id.home:
+ NavUtils.navigateUpFromSameTask(this);
+ return true;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ @Override
+ public void onConfigurationChanged(Configuration newConfig) {
+ }
+
+ private void setupLoginView(View view) {
+ final EditText username = (EditText) view.findViewById(R.id.etxtUsername);
+ final EditText password = (EditText) view.findViewById(R.id.etxtPassword);
+ final Button login = (Button) view.findViewById(R.id.butLogin);
+ final TextView txtvError = (TextView) view.findViewById(R.id.txtvError);
+ final ProgressBar progressBar = (ProgressBar) view.findViewById(R.id.progBarLogin);
+
+ login.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+
+ final String usernameStr = username.getText().toString();
+ final String passwordStr = password.getText().toString();
+
+ if (AppConfig.DEBUG) Log.d(TAG, "Checking login credentials");
+ new AsyncTask<GpodnetService, Void, Void>() {
+
+ volatile Exception exception;
+
+ @Override
+ protected void onPreExecute() {
+ super.onPreExecute();
+ login.setEnabled(false);
+ progressBar.setVisibility(View.VISIBLE);
+ txtvError.setVisibility(View.GONE);
+
+ }
+
+ @Override
+ protected void onPostExecute(Void aVoid) {
+ super.onPostExecute(aVoid);
+ login.setEnabled(true);
+ progressBar.setVisibility(View.GONE);
+
+ if (exception == null) {
+ advance();
+ } else {
+ txtvError.setText(exception.getMessage());
+ txtvError.setVisibility(View.VISIBLE);
+ }
+ }
+
+ @Override
+ protected Void doInBackground(GpodnetService... params) {
+ try {
+ params[0].authenticate(usernameStr, passwordStr);
+ GpodnetAuthenticationActivity.this.username = usernameStr;
+ GpodnetAuthenticationActivity.this.password = passwordStr;
+ } catch (GpodnetServiceException e) {
+ e.printStackTrace();
+ exception = e;
+ }
+ return null;
+ }
+ }.execute(service);
+ }
+ });
+ }
+
+ private void setupDeviceView(View view) {
+ final EditText deviceID = (EditText) view.findViewById(R.id.etxtDeviceID);
+ final EditText caption = (EditText) view.findViewById(R.id.etxtCaption);
+ final Button createNewDevice = (Button) view.findViewById(R.id.butCreateNewDevice);
+ final Button chooseDevice = (Button) view.findViewById(R.id.butChooseExistingDevice);
+ final TextView txtvError = (TextView) view.findViewById(R.id.txtvError);
+ final ProgressBar progBarCreateDevice = (ProgressBar) view.findViewById(R.id.progbarCreateDevice);
+ final Spinner spinnerDevices = (Spinner) view.findViewById(R.id.spinnerChooseDevice);
+
+
+ // load device list
+ final AtomicReference<List<GpodnetDevice>> devices = new AtomicReference<List<GpodnetDevice>>();
+ new AsyncTask<GpodnetService, Void, List<GpodnetDevice>>() {
+
+ private volatile Exception exception;
+
+ @Override
+ protected void onPreExecute() {
+ super.onPreExecute();
+ chooseDevice.setEnabled(false);
+ spinnerDevices.setEnabled(false);
+ createNewDevice.setEnabled(false);
+ }
+
+ @Override
+ protected void onPostExecute(List<GpodnetDevice> gpodnetDevices) {
+ super.onPostExecute(gpodnetDevices);
+ if (gpodnetDevices != null) {
+ List<String> deviceNames = new ArrayList<String>();
+ for (GpodnetDevice device : gpodnetDevices) {
+ deviceNames.add(device.getCaption());
+ }
+ spinnerDevices.setAdapter(new ArrayAdapter<String>(GpodnetAuthenticationActivity.this,
+ android.R.layout.simple_spinner_dropdown_item, deviceNames));
+ spinnerDevices.setEnabled(true);
+ if (!deviceNames.isEmpty()) {
+ chooseDevice.setEnabled(true);
+ }
+ devices.set(gpodnetDevices);
+ createNewDevice.setEnabled(true);
+ }
+ }
+
+ @Override
+ protected List<GpodnetDevice> doInBackground(GpodnetService... params) {
+ try {
+ return params[0].getDevices(username);
+ } catch (GpodnetServiceException e) {
+ e.printStackTrace();
+ exception = e;
+ return null;
+ }
+ }
+ }.execute(service);
+
+
+ createNewDevice.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (checkDeviceIDText(deviceID, txtvError, devices.get())) {
+ final String deviceStr = deviceID.getText().toString();
+ final String captionStr = caption.getText().toString();
+
+ new AsyncTask<GpodnetService, Void, GpodnetDevice>() {
+
+ private volatile Exception exception;
+
+ @Override
+ protected void onPreExecute() {
+ super.onPreExecute();
+ createNewDevice.setEnabled(false);
+ chooseDevice.setEnabled(false);
+ progBarCreateDevice.setVisibility(View.VISIBLE);
+ txtvError.setVisibility(View.GONE);
+ }
+
+ @Override
+ protected void onPostExecute(GpodnetDevice result) {
+ super.onPostExecute(result);
+ createNewDevice.setEnabled(true);
+ chooseDevice.setEnabled(true);
+ progBarCreateDevice.setVisibility(View.GONE);
+ if (exception == null) {
+ selectedDevice = result;
+ advance();
+ } else {
+ txtvError.setText(exception.getMessage());
+ txtvError.setVisibility(View.VISIBLE);
+ }
+ }
+
+ @Override
+ protected GpodnetDevice doInBackground(GpodnetService... params) {
+ try {
+ params[0].configureDevice(username, deviceStr, captionStr, GpodnetDevice.DeviceType.MOBILE);
+ return new GpodnetDevice(deviceStr, captionStr, GpodnetDevice.DeviceType.MOBILE.toString(), 0);
+ } catch (GpodnetServiceException e) {
+ e.printStackTrace();
+ exception = e;
+ }
+ return null;
+ }
+ }.execute(service);
+ }
+ }
+ });
+
+ deviceID.setText(generateDeviceID());
+ chooseDevice.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ final int position = spinnerDevices.getSelectedItemPosition();
+ selectedDevice = devices.get().get(position);
+ advance();
+ }
+ });
+ }
+
+
+ private String generateDeviceID() {
+ final int DEVICE_ID_LENGTH = 10;
+ StringBuilder buffer = new StringBuilder(DEVICE_ID_LENGTH);
+ SecureRandom random = new SecureRandom();
+ for (int i = 0; i < DEVICE_ID_LENGTH; i++) {
+ buffer.append(random.nextInt(10));
+
+ }
+ return buffer.toString();
+ }
+
+ private boolean checkDeviceIDText(EditText deviceID, TextView txtvError, List<GpodnetDevice> devices) {
+ String text = deviceID.getText().toString();
+ if (text.length() == 0) {
+ txtvError.setText(R.string.gpodnetauth_device_errorEmpty);
+ txtvError.setVisibility(View.VISIBLE);
+ return false;
+ } else {
+ if (devices != null) {
+ for (GpodnetDevice device : devices) {
+ if (device.getId().equals(text)) {
+ txtvError.setText(R.string.gpodnetauth_device_errorAlreadyUsed);
+ txtvError.setVisibility(View.VISIBLE);
+ return false;
+ }
+ }
+ txtvError.setVisibility(View.GONE);
+ return true;
+ }
+ return true;
+ }
+
+ }
+
+ private void setupFinishView(View view) {
+ final Button sync = (Button) view.findViewById(R.id.butSyncNow);
+ final Button back = (Button) view.findViewById(R.id.butGoMainscreen);
+
+ sync.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ GpodnetSyncService.sendSyncIntent(GpodnetAuthenticationActivity.this);
+ finish();
+ }
+ });
+ back.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ Intent intent = new Intent(GpodnetAuthenticationActivity.this, MainActivity.class);
+ intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ startActivity(intent);
+ }
+ });
+ }
+
+ private void writeLoginCredentials() {
+ if (AppConfig.DEBUG) Log.d(TAG, "Writing login credentials");
+ GpodnetPreferences.setUsername(username);
+ GpodnetPreferences.setPassword(password);
+ GpodnetPreferences.setDeviceID(selectedDevice.getId());
+ }
+
+ private void advance() {
+ if (currentStep < STEP_FINISH) {
+
+ View view = views[currentStep + 1];
+ if (currentStep == STEP_DEFAULT) {
+ setupLoginView(view);
+ } else if (currentStep == STEP_LOGIN) {
+ if (username == null || password == null) {
+ throw new IllegalStateException("Username and password must not be null here");
+ } else {
+ setupDeviceView(view);
+ }
+ } else if (currentStep == STEP_DEVICE) {
+ if (selectedDevice == null) {
+ throw new IllegalStateException("Device must not be null here");
+ } else {
+ writeLoginCredentials();
+ setupFinishView(view);
+ }
+ }
+ if (currentStep != STEP_DEFAULT) {
+ viewFlipper.showNext();
+ }
+ currentStep++;
+ } else {
+ finish();
+ }
+ }
+}
diff --git a/src/de/danoeh/antennapod/activity/gpoddernet/GpodnetMainActivity.java b/src/de/danoeh/antennapod/activity/gpoddernet/GpodnetMainActivity.java
new file mode 100644
index 000000000..9535e9d32
--- /dev/null
+++ b/src/de/danoeh/antennapod/activity/gpoddernet/GpodnetMainActivity.java
@@ -0,0 +1,89 @@
+package de.danoeh.antennapod.activity.gpoddernet;
+
+import android.os.Bundle;
+import android.support.v4.app.Fragment;
+import android.support.v4.app.FragmentManager;
+import android.support.v4.app.FragmentStatePagerAdapter;
+import android.support.v4.app.NavUtils;
+import android.support.v4.view.ViewPager;
+import android.view.MenuItem;
+import de.danoeh.antennapod.R;
+import de.danoeh.antennapod.fragment.gpodnet.PodcastTopListFragment;
+import de.danoeh.antennapod.fragment.gpodnet.SuggestionListFragment;
+import de.danoeh.antennapod.fragment.gpodnet.TagListFragment;
+import de.danoeh.antennapod.preferences.GpodnetPreferences;
+
+/**
+ * Created by daniel on 22.08.13.
+ */
+public class GpodnetMainActivity extends GpodnetActivity {
+ private static final String TAG = "GPodnetMainActivity";
+
+ private static final int POS_TAGS = 0;
+ private static final int POS_TOPLIST = 1;
+ private static final int POS_SUGGESTIONS = 2;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ getSupportActionBar().setDisplayHomeAsUpEnabled(true);
+ setContentView(R.layout.gpodnet_main);
+ ViewPager viewpager = (ViewPager) findViewById(R.id.viewpager);
+ viewpager.setAdapter(new PagerAdapter(getSupportFragmentManager()));
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case android.R.id.home:
+ NavUtils.navigateUpFromSameTask(this);
+ return true;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ private class PagerAdapter extends FragmentStatePagerAdapter {
+
+ private static final int NUM_PAGES_LOGGED_OUT = 2;
+ private static final int NUM_PAGES_LOGGED_IN = 3;
+ private final int NUM_PAGES;
+
+ public PagerAdapter(FragmentManager fm) {
+ super(fm);
+ NUM_PAGES = NUM_PAGES_LOGGED_OUT;
+ }
+
+ @Override
+ public Fragment getItem(int i) {
+ switch (i) {
+ case POS_TAGS:
+ return new TagListFragment();
+ case POS_TOPLIST:
+ return new PodcastTopListFragment();
+ case POS_SUGGESTIONS:
+ return new SuggestionListFragment();
+ default:
+ return null;
+ }
+ }
+
+ @Override
+ public CharSequence getPageTitle(int position) {
+ switch (position) {
+ case POS_TAGS:
+ return getString(R.string.gpodnet_taglist_header);
+ case POS_TOPLIST:
+ return getString(R.string.gpodnet_toplist_header);
+ case POS_SUGGESTIONS:
+ return getString(R.string.gpodnet_suggestions_header);
+ default:
+ return super.getPageTitle(position);
+ }
+ }
+
+ @Override
+ public int getCount() {
+ return NUM_PAGES;
+ }
+ }
+}
diff --git a/src/de/danoeh/antennapod/activity/gpoddernet/GpodnetSearchActivity.java b/src/de/danoeh/antennapod/activity/gpoddernet/GpodnetSearchActivity.java
new file mode 100644
index 000000000..199b45dc9
--- /dev/null
+++ b/src/de/danoeh/antennapod/activity/gpoddernet/GpodnetSearchActivity.java
@@ -0,0 +1,63 @@
+package de.danoeh.antennapod.activity.gpoddernet;
+
+import android.app.SearchManager;
+import android.content.Intent;
+import android.os.Bundle;
+import android.support.v4.app.FragmentTransaction;
+import android.support.v4.app.NavUtils;
+import android.view.MenuItem;
+import de.danoeh.antennapod.R;
+import de.danoeh.antennapod.fragment.gpodnet.SearchListFragment;
+import org.apache.commons.lang3.StringUtils;
+
+/**
+ * Created by daniel on 23.08.13.
+ */
+public class GpodnetSearchActivity extends GpodnetActivity {
+
+ private SearchListFragment searchFragment;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ getSupportActionBar().setDisplayHomeAsUpEnabled(true);
+ setContentView(R.layout.gpodnet_search);
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ Intent intent = getIntent();
+ if (StringUtils.equals(intent.getAction(), Intent.ACTION_SEARCH)) {
+ handleSearchRequest(intent.getStringExtra(SearchManager.QUERY));
+ }
+ }
+
+ @Override
+ protected void onNewIntent(Intent intent) {
+ setIntent(intent);
+ }
+
+ private void handleSearchRequest(String query) {
+ getSupportActionBar().setSubtitle(getString(R.string.search_term_label) + query);
+ if (searchFragment == null) {
+ FragmentTransaction transaction = getSupportFragmentManager()
+ .beginTransaction();
+ searchFragment = SearchListFragment.newInstance(query);
+ transaction.replace(R.id.searchListFragment, searchFragment);
+ transaction.commit();
+ } else {
+ searchFragment.changeQuery(query);
+ }
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case android.R.id.home:
+ NavUtils.navigateUpFromSameTask(this);
+ return true;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+}
diff --git a/src/de/danoeh/antennapod/activity/gpoddernet/GpodnetTagActivity.java b/src/de/danoeh/antennapod/activity/gpoddernet/GpodnetTagActivity.java
new file mode 100644
index 000000000..f3922f7aa
--- /dev/null
+++ b/src/de/danoeh/antennapod/activity/gpoddernet/GpodnetTagActivity.java
@@ -0,0 +1,64 @@
+package de.danoeh.antennapod.activity.gpoddernet;
+
+import android.os.Bundle;
+import android.support.v4.app.Fragment;
+import android.support.v4.app.FragmentTransaction;
+import android.support.v4.app.NavUtils;
+import android.view.MenuItem;
+import de.danoeh.antennapod.R;
+import de.danoeh.antennapod.fragment.gpodnet.PodcastListFragment;
+import de.danoeh.antennapod.fragment.gpodnet.SearchListFragment;
+import de.danoeh.antennapod.gpoddernet.GpodnetService;
+import de.danoeh.antennapod.gpoddernet.GpodnetServiceException;
+import de.danoeh.antennapod.gpoddernet.model.GpodnetPodcast;
+import de.danoeh.antennapod.gpoddernet.model.GpodnetTag;
+
+import java.util.List;
+
+/**
+ * Created by daniel on 23.08.13.
+ */
+public class GpodnetTagActivity extends GpodnetActivity{
+
+ private static final int PODCAST_COUNT = 50;
+ public static final String ARG_TAGNAME = "tagname";
+
+ private GpodnetTag tag;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ getSupportActionBar().setDisplayHomeAsUpEnabled(true);
+ setContentView(R.layout.gpodnet_tag_activity);
+
+ if (!getIntent().hasExtra(ARG_TAGNAME)) {
+ throw new IllegalArgumentException("No tagname argument");
+ }
+ tag = new GpodnetTag(getIntent().getStringExtra(ARG_TAGNAME));
+ getSupportActionBar().setTitle(tag.getName());
+
+ FragmentTransaction transaction = getSupportFragmentManager()
+ .beginTransaction();
+ Fragment taglistFragment = new TaglistFragment();
+ transaction.replace(R.id.taglistFragment, taglistFragment);
+ transaction.commit();
+ }
+
+ private class TaglistFragment extends PodcastListFragment {
+
+ @Override
+ protected List<GpodnetPodcast> loadPodcastData(GpodnetService service) throws GpodnetServiceException {
+ return service.getPodcastsForTag(tag, PODCAST_COUNT);
+ }
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case android.R.id.home:
+ NavUtils.navigateUpFromSameTask(this);
+ return true;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+}
diff --git a/src/de/danoeh/antennapod/adapter/ChapterListAdapter.java b/src/de/danoeh/antennapod/adapter/ChapterListAdapter.java
index 3e9b586ce..5a8dfb2bf 100644
--- a/src/de/danoeh/antennapod/adapter/ChapterListAdapter.java
+++ b/src/de/danoeh/antennapod/adapter/ChapterListAdapter.java
@@ -144,53 +144,6 @@ public class ChapterListAdapter extends ArrayAdapter<Chapter> {
TextView link;
}
- private LinkMovementMethod linkMovementMethod = new LinkMovementMethod() {
-
- @Override
- public boolean onTouchEvent(TextView widget, Spannable buffer,
- MotionEvent event) {
- Object text = widget.getText();
- if (text instanceof Spanned) {
- int action = event.getAction();
-
- if (action == MotionEvent.ACTION_UP
- || action == MotionEvent.ACTION_DOWN) {
- int x = (int) event.getX();
- int y = (int) event.getY();
-
- x -= widget.getTotalPaddingLeft();
- y -= widget.getTotalPaddingTop();
-
- x += widget.getScrollX();
- y += widget.getScrollY();
-
- Layout layout = widget.getLayout();
- int line = layout.getLineForVertical(y);
- int off = layout.getOffsetForHorizontal(line, x);
-
- ClickableSpan[] link = buffer.getSpans(off, off,
- ClickableSpan.class);
-
- if (link.length != 0) {
- if (action == MotionEvent.ACTION_UP) {
- link[0].onClick(widget);
- } else if (action == MotionEvent.ACTION_DOWN) {
- Selection.setSelection(buffer,
- buffer.getSpanStart(link[0]),
- buffer.getSpanEnd(link[0]));
- }
- return true;
- }
- }
-
- }
-
- return false;
-
- }
-
- };
-
@Override
public int getCount() {
// ignore invalid chapters
diff --git a/src/de/danoeh/antennapod/adapter/FeedItemlistDescriptionAdapter.java b/src/de/danoeh/antennapod/adapter/FeedItemlistDescriptionAdapter.java
new file mode 100644
index 000000000..5fb204b26
--- /dev/null
+++ b/src/de/danoeh/antennapod/adapter/FeedItemlistDescriptionAdapter.java
@@ -0,0 +1,55 @@
+package de.danoeh.antennapod.adapter;
+
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.TextView;
+import de.danoeh.antennapod.R;
+import de.danoeh.antennapod.feed.FeedItem;
+
+import java.util.List;
+
+/**
+ * Created by daniel on 24.08.13.
+ */
+public class FeedItemlistDescriptionAdapter extends ArrayAdapter<FeedItem> {
+
+ public FeedItemlistDescriptionAdapter(Context context, int resource, List<FeedItem> objects) {
+ super(context, resource, objects);
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ Holder holder;
+
+ FeedItem item = getItem(position);
+
+ // Inflate layout
+ if (convertView == null) {
+ holder = new Holder();
+ LayoutInflater inflater = (LayoutInflater) getContext()
+ .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ convertView = inflater.inflate(R.layout.itemdescription_listitem, null);
+ holder.title = (TextView) convertView.findViewById(R.id.txtvTitle);
+ holder.description = (TextView) convertView.findViewById(R.id.txtvDescription);
+
+ convertView.setTag(holder);
+ } else {
+ holder = (Holder) convertView.getTag();
+ }
+
+ holder.title.setText(item.getTitle());
+ if (item.getDescription() != null) {
+ holder.description.setText(item.getDescription());
+ }
+
+ return convertView;
+ }
+
+ static class Holder {
+ TextView title;
+ TextView description;
+ }
+}
diff --git a/src/de/danoeh/antennapod/adapter/gpodnet/PodcastListAdapter.java b/src/de/danoeh/antennapod/adapter/gpodnet/PodcastListAdapter.java
new file mode 100644
index 000000000..795b17917
--- /dev/null
+++ b/src/de/danoeh/antennapod/adapter/gpodnet/PodcastListAdapter.java
@@ -0,0 +1,63 @@
+package de.danoeh.antennapod.adapter.gpodnet;
+
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.ImageView;
+import android.widget.TextView;
+import de.danoeh.antennapod.R;
+import de.danoeh.antennapod.asynctask.ImageDiskCache;
+import de.danoeh.antennapod.gpoddernet.model.GpodnetPodcast;
+
+import java.util.List;
+
+/**
+ * Adapter for displaying a list of GPodnetPodcast-Objects.
+ */
+public class PodcastListAdapter extends ArrayAdapter<GpodnetPodcast> {
+ private final ImageDiskCache diskCache;
+ private final int thumbnailLength;
+
+ public PodcastListAdapter(Context context, int resource, List<GpodnetPodcast> objects) {
+ super(context, resource, objects);
+ diskCache = ImageDiskCache.getDefaultInstance();
+ thumbnailLength = (int) context.getResources().getDimension(R.dimen.thumbnail_length);
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ Holder holder;
+
+ GpodnetPodcast podcast = getItem(position);
+
+ // Inflate Layout
+ if (convertView == null) {
+ holder = new Holder();
+ LayoutInflater inflater = (LayoutInflater) getContext()
+ .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+
+ convertView = inflater.inflate(R.layout.gpodnet_podcast_listitem, null);
+ holder.title = (TextView) convertView.findViewById(R.id.txtvTitle);
+ holder.description = (TextView) convertView.findViewById(R.id.txtvDescription);
+ holder.image = (ImageView) convertView.findViewById(R.id.imgvCover);
+
+ convertView.setTag(holder);
+ } else {
+ holder = (Holder) convertView.getTag();
+ }
+
+ holder.title.setText(podcast.getTitle());
+ holder.description.setText(podcast.getDescription());
+ diskCache.loadThumbnailBitmap(podcast.getLogoUrl(), holder.image, thumbnailLength);
+
+ return convertView;
+ }
+
+ static class Holder {
+ TextView title;
+ TextView description;
+ ImageView image;
+ }
+}
diff --git a/src/de/danoeh/antennapod/asynctask/BitmapDecodeWorkerTask.java b/src/de/danoeh/antennapod/asynctask/BitmapDecodeWorkerTask.java
index 4380bc6ea..cb8e4d292 100644
--- a/src/de/danoeh/antennapod/asynctask/BitmapDecodeWorkerTask.java
+++ b/src/de/danoeh/antennapod/asynctask/BitmapDecodeWorkerTask.java
@@ -2,105 +2,115 @@ package de.danoeh.antennapod.asynctask;
import android.content.res.TypedArray;
import android.graphics.BitmapFactory;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.TransitionDrawable;
import android.os.Handler;
import android.util.Log;
import android.widget.ImageView;
import de.danoeh.antennapod.AppConfig;
+import de.danoeh.antennapod.PodcastApp;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.asynctask.ImageLoader.ImageWorkerTaskResource;
import de.danoeh.antennapod.util.BitmapDecoder;
public class BitmapDecodeWorkerTask extends Thread {
- protected int PREFERRED_LENGTH;
-
- /** Can be thumbnail or cover */
- protected int imageType;
-
- private static final String TAG = "BitmapDecodeWorkerTask";
- private ImageView target;
- protected CachedBitmap cBitmap;
-
- protected ImageLoader.ImageWorkerTaskResource imageResource;
-
- private Handler handler;
-
- private final int defaultCoverResource;
-
- public BitmapDecodeWorkerTask(Handler handler, ImageView target,
- ImageWorkerTaskResource imageResource, int length, int imageType) {
- super();
- this.handler = handler;
- this.target = target;
- this.imageResource = imageResource;
- this.PREFERRED_LENGTH = length;
- this.imageType = imageType;
- TypedArray res = target.getContext().obtainStyledAttributes(
- new int[] { R.attr.default_cover });
- this.defaultCoverResource = res.getResourceId(0, 0);
- res.recycle();
- }
-
- /**
- * Should return true if tag of the imageview is still the same it was
- * before the bitmap was decoded
- */
- protected boolean tagsMatching(ImageView target) {
- return target.getTag() == null
- || target.getTag() == imageResource.getImageLoaderCacheKey();
- }
-
- protected void onPostExecute() {
- // check if imageview is still supposed to display this image
- if (tagsMatching(target) && cBitmap.getBitmap() != null) {
- target.setImageBitmap(cBitmap.getBitmap());
- } else {
- if (AppConfig.DEBUG)
- Log.d(TAG, "Not displaying image");
- }
- }
-
- @Override
- public void run() {
- cBitmap = new CachedBitmap(BitmapDecoder.decodeBitmapFromWorkerTaskResource(
- PREFERRED_LENGTH, imageResource), PREFERRED_LENGTH);
- if (cBitmap.getBitmap() != null) {
- storeBitmapInCache(cBitmap);
- } else {
- Log.w(TAG, "Could not load bitmap. Using default image.");
- cBitmap = new CachedBitmap(BitmapFactory.decodeResource(
- target.getResources(), defaultCoverResource),
- PREFERRED_LENGTH);
- }
- if (AppConfig.DEBUG)
- Log.d(TAG, "Finished loading bitmaps");
-
- endBackgroundTask();
- }
-
- protected final void endBackgroundTask() {
- handler.post(new Runnable() {
-
- @Override
- public void run() {
- onPostExecute();
- }
-
- });
- }
-
- protected void onInvalidStream() {
- cBitmap = new CachedBitmap(BitmapFactory.decodeResource(
- target.getResources(), defaultCoverResource), PREFERRED_LENGTH);
- }
-
- protected void storeBitmapInCache(CachedBitmap cb) {
- ImageLoader loader = ImageLoader.getInstance();
- if (imageType == ImageLoader.IMAGE_TYPE_COVER) {
- loader.addBitmapToCoverCache(imageResource.getImageLoaderCacheKey(), cb);
- } else if (imageType == ImageLoader.IMAGE_TYPE_THUMBNAIL) {
- loader.addBitmapToThumbnailCache(imageResource.getImageLoaderCacheKey(), cb);
- }
- }
+ protected int PREFERRED_LENGTH;
+ public static final int FADE_DURATION = 500;
+
+ /**
+ * Can be thumbnail or cover
+ */
+ protected int imageType;
+
+ private static final String TAG = "BitmapDecodeWorkerTask";
+ private ImageView target;
+ protected CachedBitmap cBitmap;
+
+ protected ImageLoader.ImageWorkerTaskResource imageResource;
+
+ private Handler handler;
+
+ private final int defaultCoverResource;
+
+ public BitmapDecodeWorkerTask(Handler handler, ImageView target,
+ ImageWorkerTaskResource imageResource, int length, int imageType) {
+ super();
+ this.handler = handler;
+ this.target = target;
+ this.imageResource = imageResource;
+ this.PREFERRED_LENGTH = length;
+ this.imageType = imageType;
+ this.defaultCoverResource = android.R.color.transparent;
+ }
+
+ /**
+ * Should return true if tag of the imageview is still the same it was
+ * before the bitmap was decoded
+ */
+ protected boolean tagsMatching(ImageView target) {
+ return target.getTag(R.id.imageloader_key) == null
+ || target.getTag(R.id.imageloader_key).equals(imageResource.getImageLoaderCacheKey());
+ }
+
+ protected void onPostExecute() {
+ // check if imageview is still supposed to display this image
+ if (tagsMatching(target) && cBitmap.getBitmap() != null) {
+ Drawable[] drawables = new Drawable[]{
+ PodcastApp.getInstance().getResources().getDrawable(android.R.color.transparent),
+ new BitmapDrawable(PodcastApp.getInstance().getResources(), cBitmap.getBitmap())
+ };
+ TransitionDrawable transitionDrawable = new TransitionDrawable(drawables);
+ target.setImageDrawable(transitionDrawable);
+ transitionDrawable.startTransition(FADE_DURATION);
+ } else {
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Not displaying image");
+ }
+ }
+
+ @Override
+ public void run() {
+ cBitmap = new CachedBitmap(BitmapDecoder.decodeBitmapFromWorkerTaskResource(
+ PREFERRED_LENGTH, imageResource), PREFERRED_LENGTH);
+ if (cBitmap.getBitmap() != null) {
+ storeBitmapInCache(cBitmap);
+ } else {
+ Log.w(TAG, "Could not load bitmap. Using default image.");
+ cBitmap = new CachedBitmap(BitmapFactory.decodeResource(
+ target.getResources(), defaultCoverResource),
+ PREFERRED_LENGTH);
+ }
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Finished loading bitmaps");
+
+ endBackgroundTask();
+ }
+
+ protected final void endBackgroundTask() {
+ handler.post(new Runnable() {
+
+ @Override
+ public void run() {
+ onPostExecute();
+ }
+
+ });
+ }
+
+ protected void onInvalidStream() {
+ cBitmap = new CachedBitmap(BitmapFactory.decodeResource(
+ target.getResources(), defaultCoverResource), PREFERRED_LENGTH);
+ }
+
+ protected void storeBitmapInCache(CachedBitmap cb) {
+ ImageLoader loader = ImageLoader.getInstance();
+ if (imageType == ImageLoader.IMAGE_TYPE_COVER) {
+ loader.addBitmapToCoverCache(imageResource.getImageLoaderCacheKey(), cb);
+ } else if (imageType == ImageLoader.IMAGE_TYPE_THUMBNAIL) {
+ loader.addBitmapToThumbnailCache(imageResource.getImageLoaderCacheKey(), cb);
+ }
+ }
}
diff --git a/src/de/danoeh/antennapod/asynctask/ImageDiskCache.java b/src/de/danoeh/antennapod/asynctask/ImageDiskCache.java
new file mode 100644
index 000000000..f7f6b576f
--- /dev/null
+++ b/src/de/danoeh/antennapod/asynctask/ImageDiskCache.java
@@ -0,0 +1,391 @@
+package de.danoeh.antennapod.asynctask;
+
+import android.os.Handler;
+import android.util.Log;
+import android.util.Pair;
+import android.widget.ImageView;
+import de.danoeh.antennapod.AppConfig;
+import de.danoeh.antennapod.PodcastApp;
+import de.danoeh.antennapod.R;
+import de.danoeh.antennapod.service.download.DownloadRequest;
+import de.danoeh.antennapod.service.download.HttpDownloader;
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.lang3.StringUtils;
+
+import java.io.*;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+/**
+ * Provides local cache for storing downloaded image. An image disk cache downloads images and stores them as long
+ * as the cache is not full. Once the cache is full, the image disk cache will delete older images.
+ */
+public class ImageDiskCache {
+ private static final String TAG = "ImageDiskCache";
+
+ private static HashMap<String, ImageDiskCache> cacheSingletons = new HashMap<String, ImageDiskCache>();
+
+ /**
+ * Return a default instance of an ImageDiskCache. This cache will store data in the external cache folder.
+ */
+ public static synchronized ImageDiskCache getDefaultInstance() {
+ final String DEFAULT_PATH = "imagecache";
+ final long DEFAULT_MAX_CACHE_SIZE = 10 * 1024 * 1024;
+
+ File cacheDir = PodcastApp.getInstance().getExternalCacheDir();
+ if (cacheDir == null) {
+ return null;
+ }
+ return getInstance(new File(cacheDir, DEFAULT_PATH).getAbsolutePath(), DEFAULT_MAX_CACHE_SIZE);
+ }
+
+ /**
+ * Return an instance of an ImageDiskCache that stores images in the specified folder.
+ */
+ public static synchronized ImageDiskCache getInstance(String path, long maxCacheSize) {
+ if (path == null) {
+ throw new NullPointerException();
+ }
+ if (cacheSingletons.containsKey(path)) {
+ return cacheSingletons.get(path);
+ }
+
+ ImageDiskCache cache = cacheSingletons.get(path);
+ if (cache == null) {
+ cache = new ImageDiskCache(path, maxCacheSize);
+ cacheSingletons.put(new File(path).getAbsolutePath(), cache);
+ }
+ cacheSingletons.put(path, cache);
+ return cache;
+ }
+
+ /**
+ * Filename - cache object mapping
+ */
+ private static final String CACHE_FILE_NAME = "cachefile";
+ private ExecutorService executor;
+ private ConcurrentHashMap<String, DiskCacheObject> diskCache;
+ private final long maxCacheSize;
+ private int cacheSize;
+ private final File cacheFolder;
+ private Handler handler;
+
+ private ImageDiskCache(String path, long maxCacheSize) {
+ this.maxCacheSize = maxCacheSize;
+ this.cacheFolder = new File(path);
+ if (!cacheFolder.exists() && !cacheFolder.mkdir()) {
+ throw new IllegalArgumentException("Image disk cache could not create cache folder in: " + path);
+ }
+
+ executor = Executors.newFixedThreadPool(Runtime.getRuntime()
+ .availableProcessors());
+ handler = new Handler();
+ }
+
+ private synchronized void initCacheFolder() {
+ if (diskCache == null) {
+ if (AppConfig.DEBUG) Log.d(TAG, "Initializing cache folder");
+ File cacheFile = new File(cacheFolder, CACHE_FILE_NAME);
+ if (cacheFile.exists()) {
+ try {
+ InputStream in = new FileInputStream(cacheFile);
+ BufferedInputStream buffer = new BufferedInputStream(in);
+ ObjectInputStream objectInput = new ObjectInputStream(buffer);
+ diskCache = (ConcurrentHashMap<String, DiskCacheObject>) objectInput.readObject();
+ // calculate cache size
+ for (DiskCacheObject dco : diskCache.values()) {
+ cacheSize += dco.size;
+ }
+ deleteInvalidFiles();
+ } catch (IOException e) {
+ e.printStackTrace();
+ diskCache = new ConcurrentHashMap<String, DiskCacheObject>();
+ } catch (ClassCastException e) {
+ e.printStackTrace();
+ diskCache = new ConcurrentHashMap<String, DiskCacheObject>();
+ } catch (ClassNotFoundException e) {
+ e.printStackTrace();
+ diskCache = new ConcurrentHashMap<String, DiskCacheObject>();
+ }
+ } else {
+ diskCache = new ConcurrentHashMap<String, DiskCacheObject>();
+ }
+ }
+ }
+
+ private List<File> getCacheFileList() {
+ Collection<DiskCacheObject> values = diskCache.values();
+ List<File> files = new ArrayList<File>();
+ for (DiskCacheObject dco : values) {
+ files.add(dco.getFile());
+ }
+ files.add(new File(cacheFolder, CACHE_FILE_NAME));
+ return files;
+ }
+
+ private Pair<String, DiskCacheObject> getOldestCacheObject() {
+ Collection<String> keys = diskCache.keySet();
+ DiskCacheObject oldest = null;
+ String oldestKey = null;
+
+ for (String key : keys) {
+
+ if (oldestKey == null) {
+ oldestKey = key;
+ oldest = diskCache.get(key);
+ } else {
+ DiskCacheObject dco = diskCache.get(key);
+ if (oldest.timestamp > dco.timestamp) {
+ oldestKey = key;
+ oldest = diskCache.get(key);
+ }
+ }
+ }
+ return new Pair<String, DiskCacheObject>(oldestKey, oldest);
+ }
+
+ private synchronized void deleteCacheObject(String key, DiskCacheObject value) {
+ Log.i(TAG, "Deleting cached object: " + key);
+ diskCache.remove(key);
+ boolean result = value.getFile().delete();
+ if (!result) {
+ Log.w(TAG, "Could not delete file " + value.fileUrl);
+ }
+ cacheSize -= value.size;
+ }
+
+ private synchronized void deleteInvalidFiles() {
+ // delete files that are not stored inside the cache
+ File[] files = cacheFolder.listFiles();
+ List<File> cacheFiles = getCacheFileList();
+ for (File file : files) {
+ if (!cacheFiles.contains(file)) {
+ Log.i(TAG, "Deleting unused file: " + file.getAbsolutePath());
+ boolean result = file.delete();
+ if (!result) {
+ Log.w(TAG, "Could not delete file: " + file.getAbsolutePath());
+ }
+ }
+ }
+ }
+
+ private synchronized void cleanup() {
+ if (cacheSize > maxCacheSize) {
+ while (cacheSize > maxCacheSize) {
+ Pair<String, DiskCacheObject> oldest = getOldestCacheObject();
+ deleteCacheObject(oldest.first, oldest.second);
+ }
+ }
+ }
+
+ /**
+ * Loads a new image from the disk cache. If the image that the url points to has already been downloaded, the image will
+ * be loaded from the disk. Otherwise, the image will be downloaded first.
+ * The image will be stored in the thumbnail cache.
+ */
+ public void loadThumbnailBitmap(final String url, final ImageView target, final int length) {
+ final ImageLoader il = ImageLoader.getInstance();
+ target.setTag(R.id.image_disk_cache_key, url);
+ if (diskCache != null) {
+ DiskCacheObject dco = getFromCacheIfAvailable(url);
+ if (dco != null) {
+ il.loadThumbnailBitmap(dco.loadImage(), target, length);
+ return;
+ }
+ }
+ target.setImageResource(android.R.color.transparent);
+ executor.submit(new ImageDownloader(url) {
+ @Override
+ protected void onImageLoaded(DiskCacheObject diskCacheObject) {
+ final Object tag = target.getTag(R.id.image_disk_cache_key);
+ if (tag != null || StringUtils.equals((String) tag, url)) {
+ il.loadThumbnailBitmap(diskCacheObject.loadImage(), target, length);
+ }
+ }
+ });
+
+ }
+
+ /**
+ * Loads a new image from the disk cache. If the image that the url points to has already been downloaded, the image will
+ * be loaded from the disk. Otherwise, the image will be downloaded first.
+ * The image will be stored in the cover cache.
+ */
+ public void loadCoverBitmap(final String url, final ImageView target, final int length) {
+ final ImageLoader il = ImageLoader.getInstance();
+ target.setTag(R.id.image_disk_cache_key, url);
+ if (diskCache != null) {
+ DiskCacheObject dco = getFromCacheIfAvailable(url);
+ if (dco != null) {
+ il.loadThumbnailBitmap(dco.loadImage(), target, length);
+ return;
+ }
+ }
+ target.setImageResource(android.R.color.transparent);
+ executor.submit(new ImageDownloader(url) {
+ @Override
+ protected void onImageLoaded(DiskCacheObject diskCacheObject) {
+ final Object tag = target.getTag(R.id.image_disk_cache_key);
+ if (tag != null || StringUtils.equals((String) tag, url)) {
+ il.loadCoverBitmap(diskCacheObject.loadImage(), target, length);
+ }
+ }
+ });
+ }
+
+ private synchronized void addToDiskCache(String url, DiskCacheObject obj) {
+ if (diskCache == null) {
+ initCacheFolder();
+ }
+ if (AppConfig.DEBUG) Log.d(TAG, "Adding new image to disk cache: " + url);
+ diskCache.put(url, obj);
+ cacheSize += obj.size;
+ if (cacheSize > maxCacheSize) {
+ cleanup();
+ }
+ saveCacheInfoFile();
+ }
+
+ private synchronized void saveCacheInfoFile() {
+ OutputStream out = null;
+ try {
+ out = new BufferedOutputStream(new FileOutputStream(new File(cacheFolder, CACHE_FILE_NAME)));
+ ObjectOutputStream objOut = new ObjectOutputStream(out);
+ objOut.writeObject(diskCache);
+ } catch (IOException e) {
+ e.printStackTrace();
+ } finally {
+ IOUtils.closeQuietly(out);
+ }
+ }
+
+ private synchronized DiskCacheObject getFromCacheIfAvailable(String key) {
+ if (diskCache == null) {
+ initCacheFolder();
+ }
+ DiskCacheObject dco = diskCache.get(key);
+ if (dco != null) {
+ dco.timestamp = System.currentTimeMillis();
+ }
+ return dco;
+ }
+
+ ConcurrentHashMap<String, File> runningDownloads = new ConcurrentHashMap<String, File>();
+
+ private abstract class ImageDownloader implements Runnable {
+ private String downloadUrl;
+
+ public ImageDownloader(String downloadUrl) {
+ this.downloadUrl = downloadUrl;
+ }
+
+ protected abstract void onImageLoaded(DiskCacheObject diskCacheObject);
+
+ public void run() {
+ DiskCacheObject tmp = getFromCacheIfAvailable(downloadUrl);
+ if (tmp != null) {
+ onImageLoaded(tmp);
+ return;
+ }
+
+ DiskCacheObject dco = null;
+ File newFile = new File(cacheFolder, Integer.toString(downloadUrl.hashCode()));
+ synchronized (ImageDiskCache.this) {
+ if (runningDownloads.containsKey(newFile.getAbsolutePath())) {
+ Log.d(TAG, "Download is already running: " + newFile.getAbsolutePath());
+ return;
+ } else {
+ runningDownloads.put(newFile.getAbsolutePath(), newFile);
+ }
+ }
+ if (newFile.exists()) {
+ newFile.delete();
+ }
+
+ HttpDownloader result = downloadFile(newFile.getAbsolutePath(), downloadUrl);
+ if (result.getResult().isSuccessful()) {
+ long size = result.getDownloadRequest().getSoFar();
+
+ dco = new DiskCacheObject(newFile.getAbsolutePath(), size);
+ addToDiskCache(downloadUrl, dco);
+ if (AppConfig.DEBUG) Log.d(TAG, "Image was downloaded");
+ } else {
+ Log.w(TAG, "Download of url " + downloadUrl + " failed. Reason: " + result.getResult().getReasonDetailed() + "(" + result.getResult().getReason() + ")");
+ }
+
+ if (dco != null) {
+ final DiskCacheObject dcoRef = dco;
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ onImageLoaded(dcoRef);
+ }
+ });
+
+ }
+ runningDownloads.remove(newFile.getAbsolutePath());
+
+ }
+
+ private HttpDownloader downloadFile(String destination, String source) {
+ DownloadRequest request = new DownloadRequest(destination, source, "", 0, 0);
+ HttpDownloader downloader = new HttpDownloader(request);
+ downloader.call();
+ return downloader;
+ }
+ }
+
+ private static class DiskCacheObject implements Serializable {
+ private final String fileUrl;
+
+ /**
+ * Last usage of this image cache object.
+ */
+ private long timestamp;
+ private final long size;
+
+ public DiskCacheObject(String fileUrl, long size) {
+ if (fileUrl == null) {
+ throw new NullPointerException();
+ }
+ this.fileUrl = fileUrl;
+ this.timestamp = System.currentTimeMillis();
+ this.size = size;
+ }
+
+ public File getFile() {
+ return new File(fileUrl);
+ }
+
+ public ImageLoader.ImageWorkerTaskResource loadImage() {
+ return new ImageLoader.ImageWorkerTaskResource() {
+
+ @Override
+ public InputStream openImageInputStream() {
+ try {
+ return new FileInputStream(getFile());
+ } catch (FileNotFoundException e) {
+ e.printStackTrace();
+ }
+ return null;
+ }
+
+ @Override
+ public InputStream reopenImageInputStream(InputStream input) {
+ IOUtils.closeQuietly(input);
+ return openImageInputStream();
+ }
+
+ @Override
+ public String getImageLoaderCacheKey() {
+ return fileUrl;
+ }
+ };
+ }
+ }
+}
diff --git a/src/de/danoeh/antennapod/asynctask/ImageLoader.java b/src/de/danoeh/antennapod/asynctask/ImageLoader.java
index fb807f469..a4a9bc823 100644
--- a/src/de/danoeh/antennapod/asynctask/ImageLoader.java
+++ b/src/de/danoeh/antennapod/asynctask/ImageLoader.java
@@ -66,7 +66,7 @@ public class ImageLoader {
private ExecutorService createExecutor() {
return Executors.newFixedThreadPool(Runtime.getRuntime()
- .availableProcessors() + 1, new ThreadFactory() {
+ .availableProcessors(), new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
@@ -77,7 +77,7 @@ public class ImageLoader {
});
}
- public static ImageLoader getInstance() {
+ public static synchronized ImageLoader getInstance() {
if (singleton == null) {
singleton = new ImageLoader();
}
@@ -106,7 +106,8 @@ public class ImageLoader {
.getContext());
if (source != null && source.getImageLoaderCacheKey() != null) {
- CachedBitmap cBitmap = getBitmapFromCoverCache(source.getImageLoaderCacheKey());
+ target.setTag(R.id.imageloader_key, source.getImageLoaderCacheKey());
+ CachedBitmap cBitmap = getBitmapFromCoverCache(source.getImageLoaderCacheKey());
if (cBitmap != null && cBitmap.getLength() >= length) {
target.setImageBitmap(cBitmap.getBitmap());
} else {
@@ -143,7 +144,8 @@ public class ImageLoader {
.getContext());
if (source != null && source.getImageLoaderCacheKey() != null) {
- CachedBitmap cBitmap = getBitmapFromThumbnailCache(source.getImageLoaderCacheKey());
+ target.setTag(R.id.imageloader_key, source.getImageLoaderCacheKey());
+ CachedBitmap cBitmap = getBitmapFromThumbnailCache(source.getImageLoaderCacheKey());
if (cBitmap != null && cBitmap.getLength() >= length) {
target.setImageBitmap(cBitmap.getBitmap());
} else {
@@ -195,11 +197,7 @@ public class ImageLoader {
}
private int getDefaultCoverResource(Context context) {
- TypedArray res = context
- .obtainStyledAttributes(new int[] { R.attr.default_cover });
- final int defaultCoverResource = res.getResourceId(0, 0);
- res.recycle();
- return defaultCoverResource;
+ return android.R.color.transparent;
}
/**
diff --git a/src/de/danoeh/antennapod/asynctask/OpmlExportWorker.java b/src/de/danoeh/antennapod/asynctask/OpmlExportWorker.java
index e14e22917..745bc7079 100644
--- a/src/de/danoeh/antennapod/asynctask/OpmlExportWorker.java
+++ b/src/de/danoeh/antennapod/asynctask/OpmlExportWorker.java
@@ -1,8 +1,9 @@
package de.danoeh.antennapod.asynctask;
import java.io.File;
-import java.io.FileWriter;
+import java.io.FileOutputStream;
import java.io.IOException;
+import java.io.OutputStreamWriter;
import java.util.Arrays;
import android.annotation.SuppressLint;
@@ -16,6 +17,7 @@ import de.danoeh.antennapod.PodcastApp;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.opml.OpmlWriter;
import de.danoeh.antennapod.preferences.UserPreferences;
+import de.danoeh.antennapod.util.LangUtils;
import de.danoeh.antennapod.storage.DBReader;
/** Writes an OPML file into the export directory in the background. */
@@ -49,13 +51,21 @@ public class OpmlExportWorker extends AsyncTask<Void, Void, Void> {
output.delete();
}
}
+ OutputStreamWriter writer = null;
try {
- FileWriter writer = new FileWriter(output);
+ writer = new OutputStreamWriter(new FileOutputStream(output), LangUtils.UTF_8);
opmlWriter.writeDocument(DBReader.getFeedList(context), writer);
- writer.close();
} catch (IOException e) {
e.printStackTrace();
exception = e;
+ } finally {
+ if (writer != null) {
+ try {
+ writer.close();
+ } catch (IOException ioe) {
+ exception = ioe;
+ }
+ }
}
return null;
}
diff --git a/src/de/danoeh/antennapod/asynctask/OpmlFeedQueuer.java b/src/de/danoeh/antennapod/asynctask/OpmlFeedQueuer.java
index 4d9c9867e..64e678086 100644
--- a/src/de/danoeh/antennapod/asynctask/OpmlFeedQueuer.java
+++ b/src/de/danoeh/antennapod/asynctask/OpmlFeedQueuer.java
@@ -1,5 +1,6 @@
package de.danoeh.antennapod.asynctask;
+import java.util.Arrays;
import java.util.Date;
import android.annotation.SuppressLint;
@@ -22,7 +23,7 @@ public class OpmlFeedQueuer extends AsyncTask<Void, Void, Void> {
public OpmlFeedQueuer(Context context, int[] selection) {
super();
this.context = context;
- this.selection = selection;
+ this.selection = Arrays.copyOf(selection, selection.length);
}
@Override
diff --git a/src/de/danoeh/antennapod/asynctask/OpmlImportWorker.java b/src/de/danoeh/antennapod/asynctask/OpmlImportWorker.java
index 5af06895f..4816c25ab 100644
--- a/src/de/danoeh/antennapod/asynctask/OpmlImportWorker.java
+++ b/src/de/danoeh/antennapod/asynctask/OpmlImportWorker.java
@@ -64,6 +64,13 @@ public class OpmlImportWorker extends
@Override
protected void onPostExecute(ArrayList<OpmlElement> result) {
+ if (mReader != null) {
+ try {
+ mReader.close();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
progDialog.dismiss();
if (exception != null) {
if (AppConfig.DEBUG)
diff --git a/src/de/danoeh/antennapod/dialog/AuthenticationDialog.java b/src/de/danoeh/antennapod/dialog/AuthenticationDialog.java
new file mode 100644
index 000000000..bdb2d68ba
--- /dev/null
+++ b/src/de/danoeh/antennapod/dialog/AuthenticationDialog.java
@@ -0,0 +1,89 @@
+package de.danoeh.antennapod.dialog;
+
+import android.app.Dialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.os.Bundle;
+import android.view.View;
+import android.view.Window;
+import android.widget.Button;
+import android.widget.CheckBox;
+import android.widget.EditText;
+import de.danoeh.antennapod.R;
+
+/**
+ * Displays a dialog with a username and password text field and an optional checkbox to save username and preferences.
+ */
+public abstract class AuthenticationDialog extends Dialog {
+
+ private final int titleRes;
+ private final boolean enableUsernameField;
+ private final boolean showSaveCredentialsCheckbox;
+ private final String usernameInitialValue;
+ private final String passwordInitialValue;
+
+ public AuthenticationDialog(Context context, int titleRes, boolean enableUsernameField, boolean showSaveCredentialsCheckbox, String usernameInitialValue, String passwordInitialValue) {
+ super(context);
+ this.titleRes = titleRes;
+ this.enableUsernameField = enableUsernameField;
+ this.showSaveCredentialsCheckbox = showSaveCredentialsCheckbox;
+ this.usernameInitialValue = usernameInitialValue;
+ this.passwordInitialValue = passwordInitialValue;
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.authentication_dialog);
+ final EditText etxtUsername = (EditText) findViewById(R.id.etxtUsername);
+ final EditText etxtPassword = (EditText) findViewById(R.id.etxtPassword);
+ final CheckBox saveUsernamePassword = (CheckBox) findViewById(R.id.chkSaveUsernamePassword);
+ final Button butConfirm = (Button) findViewById(R.id.butConfirm);
+ final Button butCancel = (Button) findViewById(R.id.butCancel);
+
+ if (titleRes != 0) {
+ setTitle(titleRes);
+ } else {
+ requestWindowFeature(Window.FEATURE_NO_TITLE);
+ }
+ etxtUsername.setEnabled(enableUsernameField);
+ if (showSaveCredentialsCheckbox) {
+ saveUsernamePassword.setVisibility(View.VISIBLE);
+ } else {
+ saveUsernamePassword.setVisibility(View.GONE);
+ }
+ if (usernameInitialValue != null) {
+ etxtUsername.setText(usernameInitialValue);
+ }
+ if (passwordInitialValue != null) {
+ etxtPassword.setText(passwordInitialValue);
+ }
+ setOnCancelListener(new OnCancelListener() {
+ @Override
+ public void onCancel(DialogInterface dialog) {
+ onCancelled();
+ }
+ });
+ butCancel.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ cancel();
+ }
+ });
+ butConfirm.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ onConfirmed(etxtUsername.getText().toString(),
+ etxtPassword.getText().toString(),
+ showSaveCredentialsCheckbox && saveUsernamePassword.isChecked());
+ dismiss();
+ }
+ });
+ }
+
+ protected void onCancelled() {
+
+ }
+
+ protected abstract void onConfirmed(String username, String password, boolean saveUsernamePassword);
+}
diff --git a/src/de/danoeh/antennapod/dialog/VariableSpeedDialog.java b/src/de/danoeh/antennapod/dialog/VariableSpeedDialog.java
new file mode 100644
index 000000000..e6cbe37d1
--- /dev/null
+++ b/src/de/danoeh/antennapod/dialog/VariableSpeedDialog.java
@@ -0,0 +1,100 @@
+package de.danoeh.antennapod.dialog;
+
+import java.util.Arrays;
+import java.util.List;
+
+import android.app.AlertDialog;
+import android.content.ActivityNotFoundException;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.net.Uri;
+import de.danoeh.antennapod.R;
+import de.danoeh.antennapod.preferences.UserPreferences;
+
+public class VariableSpeedDialog {
+ private VariableSpeedDialog() {
+ }
+
+ public static void showDialog(final Context context) {
+ if (com.aocate.media.MediaPlayer.isPrestoLibraryInstalled(context)) {
+ showSpeedSelectorDialog(context);
+ } else {
+ showGetPluginDialog(context);
+ }
+ }
+
+ private static void showGetPluginDialog(final Context context) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(context);
+ builder.setTitle(R.string.no_playback_plugin_title);
+ builder.setMessage(R.string.no_playback_plugin_msg);
+ builder.setNegativeButton(R.string.close_label, null);
+ builder.setPositiveButton(R.string.download_plugin_label,
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ try {
+ Intent playStoreIntent = new Intent(
+ Intent.ACTION_VIEW,
+ Uri.parse("market://details?id=com.falconware.prestissimo"));
+ context.startActivity(playStoreIntent);
+ } catch (ActivityNotFoundException e) {
+ // this is usually thrown on an emulator if the Android market is not installed
+ e.printStackTrace();
+ }
+ }
+ });
+ builder.create().show();
+ }
+
+ private static void showSpeedSelectorDialog(final Context context) {
+ final String[] speedValues = context.getResources().getStringArray(
+ R.array.playback_speed_values);
+ // According to Java spec these get initialized to false on creation
+ final boolean[] speedChecked = new boolean[speedValues.length];
+
+ // Build the "isChecked" array so that multiChoice dialog is
+ // populated correctly
+ List<String> selectedSpeedList = Arrays.asList(UserPreferences
+ .getPlaybackSpeedArray());
+ for (int i = 0; i < speedValues.length; i++) {
+ speedChecked[i] = selectedSpeedList.contains(speedValues[i]);
+ }
+
+ AlertDialog.Builder builder = new AlertDialog.Builder(context);
+ builder.setTitle(R.string.set_playback_speed_label);
+ builder.setMultiChoiceItems(R.array.playback_speed_values,
+ speedChecked, new DialogInterface.OnMultiChoiceClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which,
+ boolean isChecked) {
+ speedChecked[which] = isChecked;
+ }
+
+ });
+ builder.setNegativeButton(android.R.string.cancel, null);
+ builder.setPositiveButton(android.R.string.ok,
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ int choiceCount = 0;
+ for (int i = 0; i < speedChecked.length; i++) {
+ if (speedChecked[i]) {
+ choiceCount++;
+ }
+ }
+ String[] newSpeedValues = new String[choiceCount];
+ int newSpeedIndex = 0;
+ for (int i = 0; i < speedChecked.length; i++) {
+ if (speedChecked[i]) {
+ newSpeedValues[newSpeedIndex++] = speedValues[i];
+ }
+ }
+
+ UserPreferences.setPlaybackSpeedArray(newSpeedValues);
+
+ }
+ });
+ builder.create().show();
+ }
+}
diff --git a/src/de/danoeh/antennapod/feed/EventDistributor.java b/src/de/danoeh/antennapod/feed/EventDistributor.java
index c538808e2..56333da52 100644
--- a/src/de/danoeh/antennapod/feed/EventDistributor.java
+++ b/src/de/danoeh/antennapod/feed/EventDistributor.java
@@ -39,7 +39,7 @@ public class EventDistributor extends Observable {
events = new ConcurrentLinkedQueue<Integer>();
}
- public static EventDistributor getInstance() {
+ public static synchronized EventDistributor getInstance() {
if (instance == null) {
instance = new EventDistributor();
}
diff --git a/src/de/danoeh/antennapod/feed/Feed.java b/src/de/danoeh/antennapod/feed/Feed.java
index 34505dda9..032930f83 100644
--- a/src/de/danoeh/antennapod/feed/Feed.java
+++ b/src/de/danoeh/antennapod/feed/Feed.java
@@ -55,7 +55,11 @@ public class Feed extends FeedFile {
super(fileUrl, downloadUrl, downloaded);
this.id = id;
this.title = title;
- this.lastUpdate = lastUpdate;
+ if (lastUpdate != null) {
+ this.lastUpdate = (Date) lastUpdate.clone();
+ } else {
+ this.lastUpdate = null;
+ }
this.link = link;
this.description = description;
this.paymentLink = paymentLink;
@@ -83,7 +87,7 @@ public class Feed extends FeedFile {
*/
public Feed(String url, Date lastUpdate) {
super(null, url, false);
- this.lastUpdate = lastUpdate;
+ this.lastUpdate = (lastUpdate != null) ? (Date) lastUpdate.clone() : null;
}
/**
@@ -315,11 +319,11 @@ public class Feed extends FeedFile {
}
public Date getLastUpdate() {
- return lastUpdate;
+ return (lastUpdate != null) ? (Date) lastUpdate.clone() : null;
}
public void setLastUpdate(Date lastUpdate) {
- this.lastUpdate = lastUpdate;
+ this.lastUpdate = (lastUpdate != null) ? (Date) lastUpdate.clone() : null;
}
public String getFeedIdentifier() {
diff --git a/src/de/danoeh/antennapod/feed/FeedItem.java b/src/de/danoeh/antennapod/feed/FeedItem.java
index 54682397e..a80460ece 100644
--- a/src/de/danoeh/antennapod/feed/FeedItem.java
+++ b/src/de/danoeh/antennapod/feed/FeedItem.java
@@ -48,6 +48,19 @@ public class FeedItem extends FeedComponent implements
this.read = true;
}
+ /**
+ * This constructor should be used for creating test objects.
+ * */
+ public FeedItem(long id, String title, String itemIdentifier, String link, Date pubDate, boolean read, Feed feed) {
+ this.id = id;
+ this.title = title;
+ this.itemIdentifier = itemIdentifier;
+ this.link = link;
+ this.pubDate = (pubDate != null) ? (Date) pubDate.clone() : null;
+ this.read = read;
+ this.feed = feed;
+ }
+
public void updateFromOther(FeedItem other) {
super.updateFromOther(other);
if (other.title != null) {
@@ -123,19 +136,35 @@ public class FeedItem extends FeedComponent implements
}
public Date getPubDate() {
- return pubDate;
+ if (pubDate != null) {
+ return (Date) pubDate.clone();
+ } else {
+ return null;
+ }
}
public void setPubDate(Date pubDate) {
- this.pubDate = pubDate;
+ if (pubDate != null) {
+ this.pubDate = (Date) pubDate.clone();
+ } else {
+ this.pubDate = null;
+ }
}
public FeedMedia getMedia() {
return media;
}
+ /**
+ * Sets the media object of this FeedItem. If the given
+ * FeedMedia object is not null, it's 'item'-attribute value
+ * will also be set to this item.
+ * */
public void setMedia(FeedMedia media) {
this.media = media;
+ if (media != null && media.getItem() != this) {
+ media.setItem(this);
+ }
}
public Feed getFeed() {
diff --git a/src/de/danoeh/antennapod/feed/FeedMedia.java b/src/de/danoeh/antennapod/feed/FeedMedia.java
index f140a37e6..492867983 100644
--- a/src/de/danoeh/antennapod/feed/FeedMedia.java
+++ b/src/de/danoeh/antennapod/feed/FeedMedia.java
@@ -53,7 +53,8 @@ public class FeedMedia extends FeedFile implements Playable {
this.position = position;
this.size = size;
this.mime_type = mime_type;
- this.playbackCompletionDate = playbackCompletionDate;
+ this.playbackCompletionDate = playbackCompletionDate == null
+ ? null : (Date) playbackCompletionDate.clone();
}
public FeedMedia(long id, FeedItem item) {
@@ -164,16 +165,25 @@ public class FeedMedia extends FeedFile implements Playable {
return item;
}
+ /**
+ * Sets the item object of this FeedMedia. If the given
+ * FeedItem object is not null, it's 'media'-attribute value
+ * will also be set to this media object.
+ * */
public void setItem(FeedItem item) {
this.item = item;
+ if (item != null && item.getMedia() != this) {
+ item.setMedia(this);
+ }
}
public Date getPlaybackCompletionDate() {
- return playbackCompletionDate;
- }
+ return playbackCompletionDate == null
+ ? null : (Date) playbackCompletionDate.clone(); }
public void setPlaybackCompletionDate(Date playbackCompletionDate) {
- this.playbackCompletionDate = playbackCompletionDate;
+ this.playbackCompletionDate = playbackCompletionDate == null
+ ? null : (Date) playbackCompletionDate.clone();
}
public boolean isInProgress() {
diff --git a/src/de/danoeh/antennapod/fragment/ExternalPlayerFragment.java b/src/de/danoeh/antennapod/fragment/ExternalPlayerFragment.java
index 10312b20b..933263d7d 100644
--- a/src/de/danoeh/antennapod/fragment/ExternalPlayerFragment.java
+++ b/src/de/danoeh/antennapod/fragment/ExternalPlayerFragment.java
@@ -173,6 +173,12 @@ public class ExternalPlayerFragment extends Fragment {
.newOnPlayButtonClickListener());
}
}
+
+ @Override
+ public void onPlaybackSpeedChange() {
+ // TODO Auto-generated method stub
+
+ }
};
}
diff --git a/src/de/danoeh/antennapod/fragment/FeedlistFragment.java b/src/de/danoeh/antennapod/fragment/FeedlistFragment.java
index 0e06e546e..6283a4b7f 100644
--- a/src/de/danoeh/antennapod/fragment/FeedlistFragment.java
+++ b/src/de/danoeh/antennapod/fragment/FeedlistFragment.java
@@ -1,7 +1,5 @@
package de.danoeh.antennapod.fragment;
-import java.util.List;
-
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.DialogInterface;
@@ -14,7 +12,6 @@ import android.support.v7.view.ActionMode;
import android.util.Log;
import android.view.*;
import android.widget.*;
-
import de.danoeh.antennapod.AppConfig;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.activity.FeedItemlistActivity;
@@ -29,6 +26,8 @@ import de.danoeh.antennapod.storage.DownloadRequestException;
import de.danoeh.antennapod.storage.FeedItemStatistics;
import de.danoeh.antennapod.util.menuhandler.FeedMenuHandler;
+import java.util.List;
+
public class FeedlistFragment extends Fragment implements
ActionMode.Callback, AdapterView.OnItemClickListener,
AdapterView.OnItemLongClickListener {
@@ -244,11 +243,18 @@ public class FeedlistFragment extends Fragment implements
return true;
}
+ private boolean actionModeDestroyWorkaround = false; // TODO remove this workaround
+
@Override
public void onDestroyActionMode(ActionMode mode) {
- mActionMode = null;
- selectedFeed = null;
- fla.setSelectedItemIndex(FeedlistAdapter.SELECTION_NONE);
+ if (actionModeDestroyWorkaround) {
+ mActionMode = null;
+ selectedFeed = null;
+ fla.setSelectedItemIndex(FeedlistAdapter.SELECTION_NONE);
+ actionModeDestroyWorkaround = false;
+ } else {
+ actionModeDestroyWorkaround = true;
+ }
}
@Override
@@ -265,9 +271,9 @@ public class FeedlistFragment extends Fragment implements
public boolean onItemLongClick(AdapterView<?> parent, View view,
int position, long id) {
Feed selection = fla.getItem(position);
- if (AppConfig.DEBUG)
- Log.d(TAG, "Selected Feed with title " + selection.getTitle());
if (selection != null) {
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Selected Feed with title " + selection.getTitle());
if (mActionMode != null) {
mActionMode.finish();
}
diff --git a/src/de/danoeh/antennapod/fragment/ItemDescriptionFragment.java b/src/de/danoeh/antennapod/fragment/ItemDescriptionFragment.java
index 9183180c1..c996f497e 100644
--- a/src/de/danoeh/antennapod/fragment/ItemDescriptionFragment.java
+++ b/src/de/danoeh/antennapod/fragment/ItemDescriptionFragment.java
@@ -1,5 +1,6 @@
package de.danoeh.antennapod.fragment;
+import android.content.*;
import android.support.v4.app.Fragment;
import android.support.v7.app.ActionBarActivity;
import de.danoeh.antennapod.feed.FeedItem;
@@ -9,10 +10,6 @@ import org.apache.commons.lang3.StringEscapeUtils;
import android.annotation.SuppressLint;
import android.app.Activity;
-import android.content.ClipData;
-import android.content.Context;
-import android.content.Intent;
-import android.content.SharedPreferences;
import android.content.res.TypedArray;
import android.net.Uri;
import android.os.AsyncTask;
@@ -117,7 +114,12 @@ public class ItemDescriptionFragment extends Fragment {
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
- startActivity(intent);
+ try {
+ startActivity(intent);
+ } catch (ActivityNotFoundException e) {
+ e.printStackTrace();
+ return false;
+ }
return true;
}
@@ -138,6 +140,7 @@ public class ItemDescriptionFragment extends Fragment {
}
});
+
registerForContextMenu(webvDescription);
return webvDescription;
}
@@ -371,11 +374,10 @@ public class ItemDescriptionFragment extends Fragment {
Callable<String> shownotesLoadTask = shownotesProvider.loadShownotes();
final String shownotes = shownotesLoadTask.call();
- data = "";
data = StringEscapeUtils.unescapeHtml4(shownotes);
Activity activity = getActivity();
if (activity != null) {
- TypedArray res = getActivity()
+ TypedArray res = activity
.getTheme()
.obtainStyledAttributes(
new int[]{android.R.attr.textColorPrimary});
diff --git a/src/de/danoeh/antennapod/fragment/ItemlistFragment.java b/src/de/danoeh/antennapod/fragment/ItemlistFragment.java
index 40637544d..282bb4d5c 100644
--- a/src/de/danoeh/antennapod/fragment/ItemlistFragment.java
+++ b/src/de/danoeh/antennapod/fragment/ItemlistFragment.java
@@ -50,7 +50,6 @@ public class ItemlistFragment extends ListFragment {
public static final String EXTRA_SELECTED_FEEDITEM = "extra.de.danoeh.antennapod.activity.selected_feeditem";
public static final String ARGUMENT_FEED_ID = "argument.de.danoeh.antennapod.feed_id";
protected InternalFeedItemlistAdapter fila;
- protected DownloadRequester requester = DownloadRequester.getInstance();
private Feed feed;
protected List<Long> queue;
@@ -61,6 +60,8 @@ public class ItemlistFragment extends ListFragment {
/** Argument for FeeditemlistAdapter */
protected boolean showFeedtitle;
+ private AsyncTask<Long, Void, Feed> currentLoadTask;
+
public ItemlistFragment(boolean showFeedtitle) {
super();
this.showFeedtitle = showFeedtitle;
@@ -116,11 +117,21 @@ public class ItemlistFragment extends ListFragment {
return inflater.inflate(R.layout.feeditemlist, container, false);
}
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- loadData();
- }
+ @Override
+ public void onStart() {
+ super.onStart();
+ EventDistributor.getInstance().register(contentUpdate);
+ loadData();
+ }
+
+ @Override
+ public void onDestroyView() {
+ super.onDestroyView();
+ EventDistributor.getInstance().unregister(contentUpdate);
+ if (currentLoadTask != null) {
+ currentLoadTask.cancel(true);
+ }
+ }
protected void loadData() {
final long feedId;
@@ -156,8 +167,6 @@ public class ItemlistFragment extends ListFragment {
} else {
Log.e(TAG, "Could not load queue");
}
- if (result.getItems().isEmpty()) {
- }
setEmptyViewIfListIsEmpty();
if (fila != null) {
fila.notifyDataSetChanged();
@@ -171,6 +180,7 @@ public class ItemlistFragment extends ListFragment {
}
}
};
+ currentLoadTask = loadTask;
loadTask.execute(feedId);
}
@@ -188,17 +198,6 @@ public class ItemlistFragment extends ListFragment {
}
@Override
- public void onPause() {
- super.onPause();
- }
-
- @Override
- public void onDestroy() {
- super.onDestroy();
- EventDistributor.getInstance().unregister(contentUpdate);
- }
-
- @Override
public void onResume() {
super.onResume();
getActivity().runOnUiThread(new Runnable() {
@@ -209,7 +208,6 @@ public class ItemlistFragment extends ListFragment {
}
});
updateProgressBarVisibility();
- EventDistributor.getInstance().register(contentUpdate);
}
@Override
diff --git a/src/de/danoeh/antennapod/fragment/gpodnet/PodcastListFragment.java b/src/de/danoeh/antennapod/fragment/gpodnet/PodcastListFragment.java
new file mode 100644
index 000000000..32e11e0ce
--- /dev/null
+++ b/src/de/danoeh/antennapod/fragment/gpodnet/PodcastListFragment.java
@@ -0,0 +1,120 @@
+package de.danoeh.antennapod.fragment.gpodnet;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.support.v4.app.Fragment;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.GridView;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+import de.danoeh.antennapod.AppConfig;
+import de.danoeh.antennapod.R;
+import de.danoeh.antennapod.activity.DefaultOnlineFeedViewActivity;
+import de.danoeh.antennapod.activity.OnlineFeedViewActivity;
+import de.danoeh.antennapod.adapter.gpodnet.PodcastListAdapter;
+import de.danoeh.antennapod.gpoddernet.GpodnetService;
+import de.danoeh.antennapod.gpoddernet.GpodnetServiceException;
+import de.danoeh.antennapod.gpoddernet.model.GpodnetPodcast;
+
+import java.util.List;
+
+/**
+ * Displays a list of GPodnetPodcast-Objects in a GridView
+ */
+public abstract class PodcastListFragment extends Fragment {
+ private static final String TAG = "PodcastListFragment";
+
+ private GridView gridView;
+ private ProgressBar progressBar;
+ private TextView txtvError;
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ setRetainInstance(true);
+ View root = inflater.inflate(R.layout.gpodnet_podcast_list, container, false);
+
+ gridView = (GridView) root.findViewById(R.id.gridView);
+ progressBar = (ProgressBar) root.findViewById(R.id.progressBar);
+ txtvError = (TextView) root.findViewById(R.id.txtvError);
+
+ gridView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
+ @Override
+ public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+ onPodcastSelected((GpodnetPodcast) gridView.getAdapter().getItem(position));
+ }
+ });
+
+ loadData();
+ return root;
+ }
+
+ protected void onPodcastSelected(GpodnetPodcast selection) {
+ if (AppConfig.DEBUG) Log.d(TAG, "Selected podcast: " + selection.toString());
+ Intent intent = new Intent(getActivity(), DefaultOnlineFeedViewActivity.class);
+ intent.putExtra(OnlineFeedViewActivity.ARG_FEEDURL, selection.getUrl());
+ intent.putExtra(DefaultOnlineFeedViewActivity.ARG_TITLE, getString(R.string.gpodnet_main_label));
+ startActivity(intent);
+ }
+
+ protected abstract List<GpodnetPodcast> loadPodcastData(GpodnetService service) throws GpodnetServiceException;
+
+ protected final void loadData() {
+ AsyncTask<Void, Void, List<GpodnetPodcast>> loaderTask = new AsyncTask<Void, Void, List<GpodnetPodcast>>() {
+ volatile Exception exception = null;
+
+ @Override
+ protected List<GpodnetPodcast> doInBackground(Void... params) {
+ GpodnetService service = null;
+ try {
+ service = new GpodnetService();
+ return loadPodcastData(service);
+ } catch (GpodnetServiceException e) {
+ exception = e;
+ e.printStackTrace();
+ return null;
+ } finally {
+ if (service != null) {
+ service.shutdown();
+ }
+ }
+ }
+
+ @Override
+ protected void onPostExecute(List<GpodnetPodcast> gpodnetPodcasts) {
+ super.onPostExecute(gpodnetPodcasts);
+ final Context context = getActivity();
+ if (context != null && gpodnetPodcasts != null) {
+ PodcastListAdapter listAdapter = new PodcastListAdapter(context, 0, gpodnetPodcasts);
+ gridView.setAdapter(listAdapter);
+ listAdapter.notifyDataSetChanged();
+
+ progressBar.setVisibility(View.GONE);
+ gridView.setVisibility(View.VISIBLE);
+ } else if (context != null) {
+ gridView.setVisibility(View.GONE);
+ progressBar.setVisibility(View.GONE);
+ txtvError.setText(getString(R.string.error_msg_prefix) + exception.getMessage());
+ }
+ }
+
+ @Override
+ protected void onPreExecute() {
+ super.onPreExecute();
+ gridView.setVisibility(View.GONE);
+ progressBar.setVisibility(View.VISIBLE);
+ }
+ };
+
+ if (android.os.Build.VERSION.SDK_INT > android.os.Build.VERSION_CODES.GINGERBREAD_MR1) {
+ loaderTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ } else {
+ loaderTask.execute();
+ }
+ }
+}
diff --git a/src/de/danoeh/antennapod/fragment/gpodnet/PodcastTopListFragment.java b/src/de/danoeh/antennapod/fragment/gpodnet/PodcastTopListFragment.java
new file mode 100644
index 000000000..7007d0b9a
--- /dev/null
+++ b/src/de/danoeh/antennapod/fragment/gpodnet/PodcastTopListFragment.java
@@ -0,0 +1,22 @@
+package de.danoeh.antennapod.fragment.gpodnet;
+
+import android.util.Log;
+import de.danoeh.antennapod.AppConfig;
+import de.danoeh.antennapod.gpoddernet.GpodnetService;
+import de.danoeh.antennapod.gpoddernet.GpodnetServiceException;
+import de.danoeh.antennapod.gpoddernet.model.GpodnetPodcast;
+
+import java.util.List;
+
+/**
+ *
+ */
+public class PodcastTopListFragment extends PodcastListFragment {
+ private static final String TAG = "PodcastTopListFragment";
+ private static final int PODCAST_COUNT = 50;
+
+ @Override
+ protected List<GpodnetPodcast> loadPodcastData(GpodnetService service) throws GpodnetServiceException {
+ return service.getPodcastToplist(PODCAST_COUNT);
+ }
+}
diff --git a/src/de/danoeh/antennapod/fragment/gpodnet/SearchListFragment.java b/src/de/danoeh/antennapod/fragment/gpodnet/SearchListFragment.java
new file mode 100644
index 000000000..322d13097
--- /dev/null
+++ b/src/de/danoeh/antennapod/fragment/gpodnet/SearchListFragment.java
@@ -0,0 +1,48 @@
+package de.danoeh.antennapod.fragment.gpodnet;
+
+import android.os.Bundle;
+import de.danoeh.antennapod.gpoddernet.GpodnetService;
+import de.danoeh.antennapod.gpoddernet.GpodnetServiceException;
+import de.danoeh.antennapod.gpoddernet.model.GpodnetPodcast;
+
+import java.util.List;
+
+/**
+ * Created by daniel on 23.08.13.
+ */
+public class SearchListFragment extends PodcastListFragment {
+ private static final String ARG_QUERY = "query";
+
+ private String query;
+
+ public static SearchListFragment newInstance(String query) {
+ SearchListFragment fragment = new SearchListFragment();
+ Bundle args = new Bundle();
+ args.putString(ARG_QUERY, query);
+ fragment.setArguments(args);
+ return fragment;
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ if (getArguments() != null && getArguments().containsKey(ARG_QUERY)) {
+ this.query = getArguments().getString(ARG_QUERY);
+ } else {
+ this.query = "";
+ }
+ }
+
+ @Override
+ protected List<GpodnetPodcast> loadPodcastData(GpodnetService service) throws GpodnetServiceException {
+ return service.searchPodcasts(query, 0);
+ }
+
+ public void changeQuery(String query) {
+ if (query == null) {
+ throw new NullPointerException();
+ }
+ this.query = query;
+ loadData();
+ }
+}
diff --git a/src/de/danoeh/antennapod/fragment/gpodnet/SuggestionListFragment.java b/src/de/danoeh/antennapod/fragment/gpodnet/SuggestionListFragment.java
new file mode 100644
index 000000000..45fe25580
--- /dev/null
+++ b/src/de/danoeh/antennapod/fragment/gpodnet/SuggestionListFragment.java
@@ -0,0 +1,26 @@
+package de.danoeh.antennapod.fragment.gpodnet;
+
+import de.danoeh.antennapod.gpoddernet.GpodnetService;
+import de.danoeh.antennapod.gpoddernet.GpodnetServiceException;
+import de.danoeh.antennapod.gpoddernet.model.GpodnetPodcast;
+import de.danoeh.antennapod.preferences.GpodnetPreferences;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Displays suggestions from gpodder.net
+ */
+public class SuggestionListFragment extends PodcastListFragment {
+ private static final int SUGGESTIONS_COUNT = 50;
+
+ @Override
+ protected List<GpodnetPodcast> loadPodcastData(GpodnetService service) throws GpodnetServiceException {
+ if (GpodnetPreferences.loggedIn()) {
+ service.authenticate(GpodnetPreferences.getUsername(), GpodnetPreferences.getPassword());
+ return service.getSuggestions(SUGGESTIONS_COUNT);
+ } else {
+ return new ArrayList<GpodnetPodcast>();
+ }
+ }
+}
diff --git a/src/de/danoeh/antennapod/fragment/gpodnet/TagListFragment.java b/src/de/danoeh/antennapod/fragment/gpodnet/TagListFragment.java
new file mode 100644
index 000000000..3d63f2e58
--- /dev/null
+++ b/src/de/danoeh/antennapod/fragment/gpodnet/TagListFragment.java
@@ -0,0 +1,96 @@
+package de.danoeh.antennapod.fragment.gpodnet;
+
+import android.R;
+import android.content.Context;
+import android.content.Intent;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.support.v4.app.ListFragment;
+import android.view.View;
+import android.widget.AdapterView;
+import android.widget.ArrayAdapter;
+import android.widget.TextView;
+import de.danoeh.antennapod.activity.gpoddernet.GpodnetTagActivity;
+import de.danoeh.antennapod.gpoddernet.GpodnetService;
+import de.danoeh.antennapod.gpoddernet.GpodnetServiceException;
+import de.danoeh.antennapod.gpoddernet.model.GpodnetTag;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class TagListFragment extends ListFragment {
+ private static final String TAG = "TagListFragment";
+ private static final int COUNT = 50;
+
+ @Override
+ public void onViewCreated(View view, Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+ setRetainInstance(true);
+
+ getListView().setOnItemClickListener(new AdapterView.OnItemClickListener() {
+ @Override
+ public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+ String selectedTag = (String) getListAdapter().getItem(position);
+ Intent intent = new Intent(getActivity(), GpodnetTagActivity.class);
+ intent.putExtra(GpodnetTagActivity.ARG_TAGNAME, selectedTag);
+ startActivity(intent);
+ }
+ });
+
+ loadData();
+ }
+
+ private void loadData() {
+ AsyncTask<Void, Void, List<GpodnetTag>> task = new AsyncTask<Void, Void, List<GpodnetTag>>() {
+ private Exception exception;
+
+ @Override
+ protected List<GpodnetTag> doInBackground(Void... params) {
+ GpodnetService service = new GpodnetService();
+ try {
+ return service.getTopTags(COUNT);
+ } catch (GpodnetServiceException e) {
+ e.printStackTrace();
+ exception = e;
+ return null;
+ } finally {
+ service.shutdown();
+ }
+ }
+
+ @Override
+ protected void onPreExecute() {
+ super.onPreExecute();
+ setListShown(false);
+ }
+
+ @Override
+ protected void onPostExecute(List<GpodnetTag> gpodnetTags) {
+ super.onPostExecute(gpodnetTags);
+ final Context context = getActivity();
+ if (context != null) {
+ if (gpodnetTags != null) {
+ List<String> tagNames = new ArrayList<String>();
+ for (GpodnetTag tag : gpodnetTags) {
+ tagNames.add(tag.getName());
+ }
+ setListAdapter(new ArrayAdapter<String>(context, R.layout.simple_list_item_1, tagNames));
+ setListShown(true);
+ } else if (exception != null) {
+ TextView txtvError = new TextView(getActivity());
+ txtvError.setText(exception.getMessage());
+ getListView().setEmptyView(txtvError);
+ } else {
+ setListShown(true);
+ }
+ }
+ }
+ };
+ if (android.os.Build.VERSION.SDK_INT > android.os.Build.VERSION_CODES.GINGERBREAD_MR1) {
+ task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ } else {
+ task.execute();
+ }
+ }
+}
+
diff --git a/src/de/danoeh/antennapod/gpoddernet/GpodnetClient.java b/src/de/danoeh/antennapod/gpoddernet/GpodnetClient.java
new file mode 100644
index 000000000..845a23823
--- /dev/null
+++ b/src/de/danoeh/antennapod/gpoddernet/GpodnetClient.java
@@ -0,0 +1,35 @@
+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
new file mode 100644
index 000000000..7e0a34e0b
--- /dev/null
+++ b/src/de/danoeh/antennapod/gpoddernet/GpodnetService.java
@@ -0,0 +1,725 @@
+package de.danoeh.antennapod.gpoddernet;
+
+import de.danoeh.antennapod.AppConfig;
+import de.danoeh.antennapod.gpoddernet.model.*;
+import org.apache.http.Header;
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpResponse;
+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.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.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.UnsupportedEncodingException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.LinkedList;
+import java.util.List;
+
+/**
+ * Communicates with the gpodder.net service.
+ */
+public class GpodnetService {
+
+ private static final String BASE_SCHEME = "https";
+ private static final String BASE_HOST = "gpodder.net";
+
+ private GpodnetClient httpClient;
+
+ public GpodnetService() {
+ httpClient = new GpodnetClient();
+ httpClient.getParams().setParameter(CoreProtocolPNames.USER_AGENT, AppConfig.USER_AGENT);
+ }
+
+ /**
+ * Returns the [count] most used tags.
+ */
+ public List<GpodnetTag> getTopTags(int count)
+ throws GpodnetServiceException {
+ URI uri;
+ try {
+ uri = new URI(BASE_SCHEME, BASE_HOST, String.format(
+ "/api/2/tags/%d.json", count), null);
+ } catch (URISyntaxException e1) {
+ e1.printStackTrace();
+ throw new IllegalStateException(e1);
+ }
+
+ HttpGet request = new HttpGet(uri);
+ String response = executeRequest(request);
+ try {
+ JSONArray jsonTagList = new JSONArray(response);
+ List<GpodnetTag> tagList = new ArrayList<GpodnetTag>(
+ jsonTagList.length());
+ for (int i = 0; i < jsonTagList.length(); i++) {
+ JSONObject jObj = jsonTagList.getJSONObject(i);
+ String name = jObj.getString("tag");
+ int usage = jObj.getInt("usage");
+ tagList.add(new GpodnetTag(name, usage));
+ }
+ return tagList;
+ } catch (JSONException e) {
+ e.printStackTrace();
+ throw new GpodnetServiceException(e);
+ }
+ }
+
+ /**
+ * Returns the [count] most subscribed podcasts for the given tag.
+ *
+ * @throws IllegalArgumentException if tag is null
+ */
+ public List<GpodnetPodcast> getPodcastsForTag(GpodnetTag tag, int count)
+ throws GpodnetServiceException {
+ if (tag == null) {
+ throw new IllegalArgumentException(
+ "Tag and title of tag must not be null");
+ }
+ try {
+ URI uri = new URI(BASE_SCHEME, BASE_HOST, String.format(
+ "/api/2/tag/%s/%d.json", tag.getName(), count), null);
+ HttpGet request = new HttpGet(uri);
+ String response = executeRequest(request);
+
+ JSONArray jsonArray = new JSONArray(response);
+ return readPodcastListFromJSONArray(jsonArray);
+
+ } catch (JSONException e) {
+ e.printStackTrace();
+ throw new GpodnetServiceException(e);
+ } catch (URISyntaxException e) {
+ e.printStackTrace();
+ throw new GpodnetServiceException(e);
+
+ }
+ }
+
+ /**
+ * Returns the toplist of podcast.
+ *
+ * @param count of elements that should be returned. Must be in range 1..100.
+ * @throws IllegalArgumentException if count is out of range.
+ */
+ public List<GpodnetPodcast> getPodcastToplist(int count)
+ throws GpodnetServiceException {
+ if (count < 1 || count > 100) {
+ throw new IllegalArgumentException("Count must be in range 1..100");
+ }
+ try {
+ URI uri = new URI(BASE_SCHEME, BASE_HOST, String.format(
+ "/toplist/%d.json", count), null);
+ HttpGet request = new HttpGet(uri);
+ String response = executeRequest(request);
+
+ JSONArray jsonArray = new JSONArray(response);
+ return readPodcastListFromJSONArray(jsonArray);
+
+ } catch (JSONException e) {
+ e.printStackTrace();
+ throw new GpodnetServiceException(e);
+ } catch (URISyntaxException e) {
+ e.printStackTrace();
+ throw new IllegalStateException(e);
+
+ }
+ }
+
+ /**
+ * Returns a list of suggested podcasts for the user that is currently
+ * logged in.
+ * <p/>
+ * This method requires authentication.
+ *
+ * @param count The
+ * number of elements that should be returned. Must be in range
+ * 1..100.
+ * @throws IllegalArgumentException if count is out of range.
+ * @throws GpodnetServiceAuthenticationException If there is an authentication error.
+ */
+ public List<GpodnetPodcast> getSuggestions(int count) throws GpodnetServiceException {
+ if (count < 1 || count > 100) {
+ throw new IllegalArgumentException("Count must be in range 1..100");
+ }
+ try {
+ URI uri = new URI(BASE_SCHEME, BASE_HOST, String.format(
+ "/suggestions/%d.json", count), null);
+ HttpGet request = new HttpGet(uri);
+ String response = executeRequest(request);
+
+ JSONArray jsonArray = new JSONArray(response);
+ return readPodcastListFromJSONArray(jsonArray);
+ } catch (URISyntaxException e) {
+ e.printStackTrace();
+ throw new IllegalStateException(e);
+ } catch (JSONException e) {
+ e.printStackTrace();
+ throw new GpodnetServiceException(e);
+ }
+ }
+
+ /**
+ * Searches the podcast directory for a given string.
+ *
+ * @param query The search query
+ * @param scaledLogoSize The size of the logos that are returned by the search query.
+ * Must be in range 1..256. If the value is out of range, the
+ * default value defined by the gpodder.net API will be used.
+ */
+ public List<GpodnetPodcast> searchPodcasts(String query, int scaledLogoSize)
+ throws GpodnetServiceException {
+ String parameters = (scaledLogoSize > 0 && scaledLogoSize <= 256) ? String
+ .format("q=%s&scale_logo=%d", query, scaledLogoSize) : String
+ .format("q=%s", query);
+ try {
+ URI uri = new URI(BASE_SCHEME, null, BASE_HOST, -1, "/search.json",
+ parameters, null);
+ System.out.println(uri.toASCIIString());
+ HttpGet request = new HttpGet(uri);
+ String response = executeRequest(request);
+
+ JSONArray jsonArray = new JSONArray(response);
+ return readPodcastListFromJSONArray(jsonArray);
+
+ } catch (JSONException e) {
+ e.printStackTrace();
+ throw new GpodnetServiceException(e);
+ } catch (URISyntaxException e) {
+ e.printStackTrace();
+ throw new IllegalStateException(e);
+
+ }
+ }
+
+ /**
+ * Returns all devices of a given user.
+ * <p/>
+ * This method requires authentication.
+ *
+ * @param username The username. Must be the same user as the one which is
+ * currently logged in.
+ * @throws IllegalArgumentException If username is null.
+ * @throws GpodnetServiceAuthenticationException If there is an authentication error.
+ */
+ public List<GpodnetDevice> getDevices(String username)
+ throws GpodnetServiceException {
+ if (username == null) {
+ throw new IllegalArgumentException("Username must not be null");
+ }
+ try {
+ URI uri = new URI(BASE_SCHEME, BASE_HOST, String.format(
+ "/api/2/devices/%s.json", username), null);
+ HttpGet request = new HttpGet(uri);
+ String response = executeRequest(request);
+ JSONArray devicesArray = new JSONArray(response);
+ List<GpodnetDevice> result = readDeviceListFromJSONArray(devicesArray);
+
+ return result;
+ } catch (URISyntaxException e) {
+ e.printStackTrace();
+ throw new IllegalStateException(e);
+ } catch (JSONException e) {
+ e.printStackTrace();
+ throw new GpodnetServiceException(e);
+ }
+ }
+
+ /**
+ * Configures the device of a given user.
+ * <p/>
+ * This method requires authentication.
+ *
+ * @param username The username. Must be the same user as the one which is
+ * currently logged in.
+ * @param deviceId The ID of the device that should be configured.
+ * @throws IllegalArgumentException If username or deviceId is null.
+ * @throws GpodnetServiceAuthenticationException If there is an authentication error.
+ */
+ public void configureDevice(String username, String deviceId,
+ String caption, GpodnetDevice.DeviceType type)
+ throws GpodnetServiceException {
+ if (username == null || deviceId == null) {
+ throw new IllegalArgumentException(
+ "Username and device ID must not be null");
+ }
+ try {
+ URI uri = new URI(BASE_SCHEME, BASE_HOST, String.format(
+ "/api/2/devices/%s/%s.json", username, deviceId), null);
+ HttpPost request = new HttpPost(uri);
+ if (caption != null || type != null) {
+ JSONObject jsonContent = new JSONObject();
+ if (caption != null) {
+ jsonContent.put("caption", caption);
+ }
+ if (type != null) {
+ jsonContent.put("type", type.toString());
+ }
+ StringEntity strEntity = new StringEntity(
+ jsonContent.toString(), "UTF-8");
+ strEntity.setContentType("application/json");
+ request.setEntity(strEntity);
+ }
+ executeRequest(request);
+ } catch (URISyntaxException e) {
+ e.printStackTrace();
+ throw new IllegalArgumentException(e);
+ } catch (UnsupportedEncodingException e) {
+ e.printStackTrace();
+ throw new IllegalStateException(e);
+ } catch (JSONException e) {
+ e.printStackTrace();
+ throw new GpodnetServiceException(e);
+ }
+ }
+
+ /**
+ * Returns the subscriptions of a specific device.
+ * <p/>
+ * This method requires authentication.
+ *
+ * @param username The username. Must be the same user as the one which is
+ * currently logged in.
+ * @param deviceId The ID of the device whose subscriptions should be returned.
+ * @return A list of subscriptions in OPML format.
+ * @throws IllegalArgumentException If username or deviceId is null.
+ * @throws GpodnetServiceAuthenticationException If there is an authentication error.
+ */
+ public String getSubscriptionsOfDevice(String username, String deviceId)
+ throws GpodnetServiceException {
+ if (username == null || deviceId == null) {
+ throw new IllegalArgumentException(
+ "Username and device ID must not be null");
+ }
+ try {
+ URI uri = new URI(BASE_SCHEME, BASE_HOST, String.format(
+ "/subscriptions/%s/%s.opml", username, deviceId), null);
+ HttpGet request = new HttpGet(uri);
+ String response = executeRequest(request);
+ return response;
+ } catch (URISyntaxException e) {
+ e.printStackTrace();
+ throw new IllegalArgumentException(e);
+ }
+ }
+
+ /**
+ * Returns all subscriptions of a specific user.
+ * <p/>
+ * This method requires authentication.
+ *
+ * @param username The username. Must be the same user as the one which is
+ * currently logged in.
+ * @return A list of subscriptions in OPML format.
+ * @throws IllegalArgumentException If username is null.
+ * @throws GpodnetServiceAuthenticationException If there is an authentication error.
+ */
+ public String getSubscriptionsOfUser(String username)
+ throws GpodnetServiceException {
+ if (username == null) {
+ throw new IllegalArgumentException("Username must not be null");
+ }
+ try {
+ URI uri = new URI(BASE_SCHEME, BASE_HOST, String.format(
+ "/subscriptions/%s.opml", username), null);
+ HttpGet request = new HttpGet(uri);
+ String response = executeRequest(request);
+ return response;
+ } catch (URISyntaxException e) {
+ e.printStackTrace();
+ throw new IllegalArgumentException(e);
+ }
+ }
+
+ /**
+ * Uploads the subscriptions of a specific device.
+ * <p/>
+ * This method requires authentication.
+ *
+ * @param username The username. Must be the same user as the one which is
+ * currently logged in.
+ * @param deviceId The ID of the device whose subscriptions should be updated.
+ * @param subscriptions A list of feed URLs containing all subscriptions of the
+ * device.
+ * @throws IllegalArgumentException If username, deviceId or subscriptions is null.
+ * @throws GpodnetServiceAuthenticationException If there is an authentication error.
+ */
+ public void uploadSubscriptions(String username, String deviceId,
+ List<String> subscriptions) throws GpodnetServiceException {
+ if (username == null || deviceId == null || subscriptions == null) {
+ throw new IllegalArgumentException(
+ "Username, device ID and subscriptions must not be null");
+ }
+ try {
+ URI uri = new URI(BASE_SCHEME, BASE_HOST, String.format(
+ "/subscriptions/%s/%s.txt", username, deviceId), null);
+ HttpPut request = new HttpPut(uri);
+ StringBuilder builder = new StringBuilder();
+ for (String s : subscriptions) {
+ builder.append(s);
+ builder.append("\n");
+ }
+ StringEntity entity = new StringEntity(builder.toString(), "UTF-8");
+ request.setEntity(entity);
+
+ executeRequest(request);
+ } catch (URISyntaxException e) {
+ e.printStackTrace();
+ throw new IllegalStateException(e);
+ } catch (UnsupportedEncodingException e) {
+ e.printStackTrace();
+ throw new IllegalStateException(e);
+ }
+ }
+
+ /**
+ * Updates the subscription list of a specific device.
+ * <p/>
+ * This method requires authentication.
+ *
+ * @param username The username. Must be the same user as the one which is
+ * currently logged in.
+ * @param deviceId The ID of the device whose subscriptions should be updated.
+ * @param added Collection of feed URLs of added feeds. This Collection MUST NOT contain any duplicates
+ * @param removed Collection of feed URLs of removed feeds. This Collection MUST NOT contain any duplicates
+ * @return a GpodnetUploadChangesResponse. See {@link de.danoeh.antennapod.gpoddernet.model.GpodnetUploadChangesResponse}
+ * for details.
+ * @throws java.lang.IllegalArgumentException if username, deviceId, added or removed is null.
+ * @throws de.danoeh.antennapod.gpoddernet.GpodnetServiceException if added or removed contain duplicates or if there
+ * is an authentication error.
+ */
+ public GpodnetUploadChangesResponse uploadChanges(String username, String deviceId, Collection<String> added,
+ Collection<String> removed) throws GpodnetServiceException {
+ if (username == null || deviceId == null || added == null || removed == null) {
+ throw new IllegalArgumentException(
+ "Username, device ID, added and removed must not be null");
+ }
+ try {
+ URI uri = new URI(BASE_SCHEME, BASE_HOST, String.format(
+ "/api/2/subscriptions/%s/%s.json", username, deviceId), null);
+
+ final JSONObject requestObject = new JSONObject();
+ requestObject.put("add", new JSONArray(added));
+ requestObject.put("remove", new JSONArray(removed));
+
+ HttpPost request = new HttpPost(uri);
+ StringEntity entity = new StringEntity(requestObject.toString(), "UTF-8");
+ request.setEntity(entity);
+
+ final String response = executeRequest(request);
+ return GpodnetUploadChangesResponse.fromJSONObject(response);
+ } catch (URISyntaxException e) {
+ e.printStackTrace();
+ throw new IllegalStateException(e);
+ } catch (JSONException e) {
+ e.printStackTrace();
+ throw new GpodnetServiceException(e);
+ } catch (UnsupportedEncodingException e) {
+ e.printStackTrace();
+ throw new IllegalStateException(e);
+ }
+
+ }
+
+ /**
+ * Returns all subscription changes of a specific device.
+ * <p/>
+ * This method requires authentication.
+ *
+ * @param username The username. Must be the same user as the one which is
+ * currently logged in.
+ * @param deviceId The ID of the device whose subscription changes should be
+ * downloaded.
+ * @param timestamp A timestamp that can be used to receive all changes since a
+ * specific point in time.
+ * @throws IllegalArgumentException If username or deviceId is null.
+ * @throws GpodnetServiceAuthenticationException If there is an authentication error.
+ */
+ public GpodnetSubscriptionChange getSubscriptionChanges(String username,
+ String deviceId, long timestamp) throws GpodnetServiceException {
+ if (username == null || deviceId == null) {
+ throw new IllegalArgumentException(
+ "Username and device ID must not be null");
+ }
+ String params = String.format("since=%d", timestamp);
+ String path = String.format("/api/2/subscriptions/%s/%s.json",
+ username, deviceId);
+ try {
+ URI uri = new URI(BASE_SCHEME, null, BASE_HOST, -1, path, params,
+ null);
+ HttpGet request = new HttpGet(uri);
+
+ String response = executeRequest(request);
+ JSONObject changes = new JSONObject(response);
+ return readSubscriptionChangesFromJSONObject(changes);
+ } catch (URISyntaxException e) {
+ e.printStackTrace();
+ throw new IllegalStateException(e);
+ } catch (JSONException e) {
+ e.printStackTrace();
+ throw new GpodnetServiceException(e);
+ }
+
+ }
+
+ /**
+ * Logs in a specific user. This method must be called if any of the methods
+ * that require authentication is used.
+ *
+ * @throws IllegalArgumentException If username or password is null.
+ */
+ public void authenticate(String username, String password)
+ throws GpodnetServiceException {
+ if (username == null || password == null) {
+ throw new IllegalArgumentException(
+ "Username and password must not be null");
+ }
+ URI uri;
+ try {
+ uri = new URI(BASE_SCHEME, BASE_HOST, String.format(
+ "/api/2/auth/%s/login.json", username), null);
+ } catch (URISyntaxException e) {
+ e.printStackTrace();
+ throw new GpodnetServiceException();
+ }
+ HttpPost request = new HttpPost(uri);
+ executeRequestWithAuthentication(request, username, password);
+ }
+
+ /**
+ * Shuts down the GpodnetService's HTTP client. The service will be shut down in a separate thread to avoid
+ * NetworkOnMainThreadExceptions.
+ */
+ public void shutdown() {
+ new Thread() {
+ @Override
+ public void run() {
+ httpClient.getConnectionManager().shutdown();
+ }
+ }.start();
+ }
+
+ private String executeRequest(HttpRequestBase request)
+ throws GpodnetServiceException {
+ if (request == null) {
+ throw new IllegalArgumentException("request must not be null");
+ }
+ String responseString = null;
+ HttpResponse response = null;
+ try {
+ response = httpClient.execute(request);
+ checkStatusCode(response);
+ responseString = getStringFromEntity(response.getEntity());
+ } catch (ClientProtocolException e) {
+ e.printStackTrace();
+ throw new GpodnetServiceException(e);
+ } catch (IOException e) {
+ e.printStackTrace();
+ throw new GpodnetServiceException(e);
+ } finally {
+ if (response != null) {
+ try {
+ response.getEntity().consumeContent();
+ } catch (IOException e) {
+ e.printStackTrace();
+ throw new GpodnetServiceException(e);
+ }
+ }
+
+ }
+ return responseString;
+ }
+
+ private String executeRequestWithAuthentication(HttpRequestBase request,
+ String username, String password) throws GpodnetServiceException {
+ if (request == null || username == null || password == null) {
+ throw new IllegalArgumentException(
+ "request and credentials must not be null");
+ }
+ String result = null;
+ HttpResponse response = null;
+ try {
+ Header auth = new BasicScheme().authenticate(
+ new UsernamePasswordCredentials(username, password),
+ request);
+ request.addHeader(auth);
+ response = httpClient.execute(request);
+ checkStatusCode(response);
+ result = getStringFromEntity(response.getEntity());
+ } catch (ClientProtocolException e) {
+ e.printStackTrace();
+ throw new GpodnetServiceException(e);
+ } catch (IOException e) {
+ e.printStackTrace();
+ throw new GpodnetServiceException(e);
+ } catch (AuthenticationException e) {
+ e.printStackTrace();
+ throw new GpodnetServiceException(e);
+ } finally {
+ if (response != null) {
+ try {
+ response.getEntity().consumeContent();
+ } catch (IOException e) {
+ e.printStackTrace();
+ throw new GpodnetServiceException(e);
+ }
+ }
+ }
+ return result;
+ }
+
+ private String getStringFromEntity(HttpEntity entity)
+ throws GpodnetServiceException {
+ if (entity == null) {
+ throw new IllegalArgumentException("entity must not be null");
+ }
+ ByteArrayOutputStream outputStream;
+ int contentLength = (int) entity.getContentLength();
+ if (contentLength > 0) {
+ outputStream = new ByteArrayOutputStream(contentLength);
+ } else {
+ outputStream = new ByteArrayOutputStream();
+ }
+ try {
+ byte[] buffer = new byte[8 * 1024];
+ InputStream in = entity.getContent();
+ int count;
+ while ((count = in.read(buffer)) > 0) {
+ outputStream.write(buffer, 0, count);
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ throw new GpodnetServiceException(e);
+ }
+ // System.out.println(outputStream.toString());
+ return outputStream.toString();
+ }
+
+ private void checkStatusCode(HttpResponse response)
+ throws GpodnetServiceException {
+ if (response == null) {
+ throw new IllegalArgumentException("response must not be null");
+ }
+ int responseCode = response.getStatusLine().getStatusCode();
+ if (responseCode != HttpStatus.SC_OK) {
+ if (responseCode == HttpStatus.SC_UNAUTHORIZED) {
+ throw new GpodnetServiceAuthenticationException("Wrong username or password");
+ } else {
+ throw new GpodnetServiceBadStatusCodeException(
+ "Bad response code: " + responseCode, responseCode);
+ }
+ }
+ }
+
+ private List<GpodnetPodcast> readPodcastListFromJSONArray(JSONArray array)
+ throws JSONException {
+ if (array == null) {
+ throw new IllegalArgumentException("array must not be null");
+ }
+ List<GpodnetPodcast> result = new ArrayList<GpodnetPodcast>(
+ array.length());
+ for (int i = 0; i < array.length(); i++) {
+ result.add(readPodcastFromJSONObject(array.getJSONObject(i)));
+ }
+ return result;
+
+ }
+
+ private GpodnetPodcast readPodcastFromJSONObject(JSONObject object)
+ throws JSONException {
+ String url = object.getString("url");
+
+ String title;
+ Object titleObj = object.opt("title");
+ if (titleObj != null && titleObj instanceof String) {
+ title = (String) titleObj;
+ } else {
+ title = url;
+ }
+
+ String description;
+ Object descriptionObj = object.opt("description");
+ if (descriptionObj != null && descriptionObj instanceof String) {
+ description = (String) descriptionObj;
+ } else {
+ description = "";
+ }
+
+ int subscribers = object.getInt("subscribers");
+
+ Object logoUrlObj = object.opt("logo_url");
+ String logoUrl = (logoUrlObj instanceof String) ? (String) logoUrlObj
+ : null;
+ if (logoUrl == null) {
+ Object scaledLogoUrl = object.opt("scaled_logo_url");
+ if (scaledLogoUrl != null && scaledLogoUrl instanceof String) {
+ logoUrl = (String) scaledLogoUrl;
+ }
+ }
+
+ String website = null;
+ Object websiteObj = object.opt("website");
+ if (websiteObj != null && websiteObj instanceof String) {
+ website = (String) websiteObj;
+ }
+ String mygpoLink = object.getString("mygpo_link");
+ return new GpodnetPodcast(url, title, description, subscribers,
+ logoUrl, website, mygpoLink);
+ }
+
+ private List<GpodnetDevice> readDeviceListFromJSONArray(JSONArray array)
+ throws JSONException {
+ if (array == null) {
+ throw new IllegalArgumentException("array must not be null");
+ }
+ List<GpodnetDevice> result = new ArrayList<GpodnetDevice>(
+ array.length());
+ for (int i = 0; i < array.length(); i++) {
+ result.add(readDeviceFromJSONObject(array.getJSONObject(i)));
+ }
+ return result;
+ }
+
+ private GpodnetDevice readDeviceFromJSONObject(JSONObject object)
+ throws JSONException {
+ String id = object.getString("id");
+ String caption = object.getString("caption");
+ String type = object.getString("type");
+ int subscriptions = object.getInt("subscriptions");
+ return new GpodnetDevice(id, caption, type, subscriptions);
+ }
+
+ private GpodnetSubscriptionChange readSubscriptionChangesFromJSONObject(
+ JSONObject object) throws JSONException {
+ if (object == null) {
+ throw new IllegalArgumentException("object must not be null");
+ }
+ List<String> added = new LinkedList<String>();
+ JSONArray jsonAdded = object.getJSONArray("add");
+ for (int i = 0; i < jsonAdded.length(); i++) {
+ added.add(jsonAdded.getString(i));
+ }
+
+ List<String> removed = new LinkedList<String>();
+ JSONArray jsonRemoved = object.getJSONArray("remove");
+ for (int i = 0; i < jsonRemoved.length(); i++) {
+ removed.add(jsonRemoved.getString(i));
+ }
+
+ long timestamp = object.getLong("timestamp");
+ return new GpodnetSubscriptionChange(added, removed, timestamp);
+ }
+}
diff --git a/src/de/danoeh/antennapod/gpoddernet/GpodnetServiceAuthenticationException.java b/src/de/danoeh/antennapod/gpoddernet/GpodnetServiceAuthenticationException.java
new file mode 100644
index 000000000..3b0140826
--- /dev/null
+++ b/src/de/danoeh/antennapod/gpoddernet/GpodnetServiceAuthenticationException.java
@@ -0,0 +1,21 @@
+package de.danoeh.antennapod.gpoddernet;
+
+public class GpodnetServiceAuthenticationException extends GpodnetServiceException {
+
+ public GpodnetServiceAuthenticationException() {
+ super();
+ }
+
+ public GpodnetServiceAuthenticationException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+ public GpodnetServiceAuthenticationException(String message) {
+ super(message);
+ }
+
+ public GpodnetServiceAuthenticationException(Throwable cause) {
+ super(cause);
+ }
+
+}
diff --git a/src/de/danoeh/antennapod/gpoddernet/GpodnetServiceBadStatusCodeException.java b/src/de/danoeh/antennapod/gpoddernet/GpodnetServiceBadStatusCodeException.java
new file mode 100644
index 000000000..a32e9357b
--- /dev/null
+++ b/src/de/danoeh/antennapod/gpoddernet/GpodnetServiceBadStatusCodeException.java
@@ -0,0 +1,12 @@
+package de.danoeh.antennapod.gpoddernet;
+
+public class GpodnetServiceBadStatusCodeException extends GpodnetServiceException {
+ int statusCode;
+
+ public GpodnetServiceBadStatusCodeException(String message, int statusCode) {
+ super(message);
+ this.statusCode = statusCode;
+ }
+
+
+}
diff --git a/src/de/danoeh/antennapod/gpoddernet/GpodnetServiceException.java b/src/de/danoeh/antennapod/gpoddernet/GpodnetServiceException.java
new file mode 100644
index 000000000..bdb394454
--- /dev/null
+++ b/src/de/danoeh/antennapod/gpoddernet/GpodnetServiceException.java
@@ -0,0 +1,19 @@
+package de.danoeh.antennapod.gpoddernet;
+
+public class GpodnetServiceException extends Exception {
+
+ public GpodnetServiceException() {
+ }
+
+ public GpodnetServiceException(String message) {
+ super(message);
+ }
+
+ public GpodnetServiceException(Throwable cause) {
+ super(cause);
+ }
+
+ public GpodnetServiceException(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/src/de/danoeh/antennapod/gpoddernet/model/GpodnetDevice.java b/src/de/danoeh/antennapod/gpoddernet/model/GpodnetDevice.java
new file mode 100644
index 000000000..ae7199fcc
--- /dev/null
+++ b/src/de/danoeh/antennapod/gpoddernet/model/GpodnetDevice.java
@@ -0,0 +1,72 @@
+package de.danoeh.antennapod.gpoddernet.model;
+
+public class GpodnetDevice {
+
+ private String id;
+ private String caption;
+ private DeviceType type;
+ private int subscriptions;
+
+ public GpodnetDevice(String id, String caption, String type,
+ int subscriptions) {
+ if (id == null) {
+ throw new IllegalArgumentException("ID must not be null");
+ }
+
+ this.id = id;
+ this.caption = caption;
+ this.type = DeviceType.fromString(type);
+ this.subscriptions = subscriptions;
+ }
+
+ @Override
+ public String toString() {
+ return "GpodnetDevice [id=" + id + ", caption=" + caption + ", type="
+ + type + ", subscriptions=" + subscriptions + "]";
+ }
+
+ public static enum DeviceType {
+ DESKTOP, LAPTOP, MOBILE, SERVER, OTHER;
+
+ static DeviceType fromString(String s) {
+ if (s == null) {
+ return OTHER;
+ }
+
+ if (s.equals("desktop")) {
+ return DESKTOP;
+ } else if (s.equals("laptop")) {
+ return LAPTOP;
+ } else if (s.equals("mobile")) {
+ return MOBILE;
+ } else if (s.equals("server")) {
+ return SERVER;
+ } else {
+ return OTHER;
+ }
+ }
+
+ @Override
+ public String toString() {
+ return super.toString().toLowerCase();
+ }
+
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ public String getCaption() {
+ return caption;
+ }
+
+ public DeviceType getType() {
+ return type;
+ }
+
+ public int getSubscriptions() {
+ return subscriptions;
+ }
+
+}
diff --git a/src/de/danoeh/antennapod/gpoddernet/model/GpodnetPodcast.java b/src/de/danoeh/antennapod/gpoddernet/model/GpodnetPodcast.java
new file mode 100644
index 000000000..aa01b66e2
--- /dev/null
+++ b/src/de/danoeh/antennapod/gpoddernet/model/GpodnetPodcast.java
@@ -0,0 +1,64 @@
+package de.danoeh.antennapod.gpoddernet.model;
+
+public class GpodnetPodcast {
+ private String url;
+ private String title;
+ private String description;
+ private int subscribers;
+ private String logoUrl;
+ private String website;
+ private String mygpoLink;
+
+ public GpodnetPodcast(String url, String title, String description,
+ int subscribers, String logoUrl, String website, String mygpoLink) {
+ if (url == null || title == null || description == null) {
+ throw new IllegalArgumentException(
+ "URL, title and description must not be null");
+ }
+
+ this.url = url;
+ this.title = title;
+ this.description = description;
+ this.subscribers = subscribers;
+ this.logoUrl = logoUrl;
+ this.website = website;
+ this.mygpoLink = mygpoLink;
+ }
+
+ @Override
+ public String toString() {
+ return "GpodnetPodcast [url=" + url + ", title=" + title
+ + ", description=" + description + ", subscribers="
+ + subscribers + ", logoUrl=" + logoUrl + ", website=" + website
+ + ", mygpoLink=" + mygpoLink + "]";
+ }
+
+ public String getUrl() {
+ return url;
+ }
+
+ public String getTitle() {
+ return title;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ public int getSubscribers() {
+ return subscribers;
+ }
+
+ public String getLogoUrl() {
+ return logoUrl;
+ }
+
+ public String getWebsite() {
+ return website;
+ }
+
+ public String getMygpoLink() {
+ return mygpoLink;
+ }
+
+}
diff --git a/src/de/danoeh/antennapod/gpoddernet/model/GpodnetSubscriptionChange.java b/src/de/danoeh/antennapod/gpoddernet/model/GpodnetSubscriptionChange.java
new file mode 100644
index 000000000..dccb53e5d
--- /dev/null
+++ b/src/de/danoeh/antennapod/gpoddernet/model/GpodnetSubscriptionChange.java
@@ -0,0 +1,40 @@
+package de.danoeh.antennapod.gpoddernet.model;
+
+import java.util.List;
+
+public class GpodnetSubscriptionChange {
+ private List<String> added;
+ private List<String> removed;
+ private long timestamp;
+
+ public GpodnetSubscriptionChange(List<String> added, List<String> removed,
+ long timestamp) {
+ if (added == null || removed == null) {
+ throw new IllegalArgumentException(
+ "added and remove must not be null");
+ }
+ this.added = added;
+ this.removed = removed;
+ this.timestamp = timestamp;
+ }
+
+ @Override
+ public String toString() {
+ return "GpodnetSubscriptionChange [added=" + added.toString()
+ + ", removed=" + removed.toString() + ", timestamp="
+ + timestamp + "]";
+ }
+
+ public List<String> getAdded() {
+ return added;
+ }
+
+ public List<String> getRemoved() {
+ return removed;
+ }
+
+ public long getTimestamp() {
+ return timestamp;
+ }
+
+}
diff --git a/src/de/danoeh/antennapod/gpoddernet/model/GpodnetTag.java b/src/de/danoeh/antennapod/gpoddernet/model/GpodnetTag.java
new file mode 100644
index 000000000..e8a36a554
--- /dev/null
+++ b/src/de/danoeh/antennapod/gpoddernet/model/GpodnetTag.java
@@ -0,0 +1,46 @@
+package de.danoeh.antennapod.gpoddernet.model;
+
+import java.util.Comparator;
+
+public class GpodnetTag {
+
+ private String name;
+ private int usage;
+
+ public GpodnetTag(String name, int usage) {
+ if (name == null) {
+ throw new IllegalArgumentException("Name must not be null");
+ }
+
+ this.name = name;
+ this.usage = usage;
+ }
+
+ public GpodnetTag(String name) {
+ super();
+ this.name = name;
+ }
+
+ @Override
+ public String toString() {
+ return "GpodnetTag [name=" + name + ", usage=" + usage + "]";
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public int getUsage() {
+ return usage;
+ }
+
+ public static class UsageComparator implements Comparator<GpodnetTag> {
+
+ @Override
+ public int compare(GpodnetTag o1, GpodnetTag o2) {
+ return o1.usage - o2.usage;
+ }
+
+ }
+
+}
diff --git a/src/de/danoeh/antennapod/gpoddernet/model/GpodnetUploadChangesResponse.java b/src/de/danoeh/antennapod/gpoddernet/model/GpodnetUploadChangesResponse.java
new file mode 100644
index 000000000..fee8c7d28
--- /dev/null
+++ b/src/de/danoeh/antennapod/gpoddernet/model/GpodnetUploadChangesResponse.java
@@ -0,0 +1,56 @@
+package de.danoeh.antennapod.gpoddernet.model;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Object returned by {@link de.danoeh.antennapod.gpoddernet.GpodnetService} in uploadChanges method.
+ */
+public class GpodnetUploadChangesResponse {
+
+ /**
+ * timestamp/ID that can be used for requesting changes since this upload.
+ */
+ public final long timestamp;
+
+ /**
+ * URLs that should be updated. The key of the map is the original URL, the value of the map
+ * is the sanitized URL.
+ */
+ public final Map<String, String> updatedUrls;
+
+ public GpodnetUploadChangesResponse(long timestamp, Map<String, String> updatedUrls) {
+ this.timestamp = timestamp;
+ this.updatedUrls = updatedUrls;
+ }
+
+ /**
+ * Creates a new GpodnetUploadChangesResponse-object from a JSON object that was
+ * returned by an uploadChanges call.
+ *
+ * @throws org.json.JSONException If the method could not parse the JSONObject.
+ */
+ public static GpodnetUploadChangesResponse fromJSONObject(String objectString) throws JSONException {
+ final JSONObject object = new JSONObject(objectString);
+ final long timestamp = object.getLong("timestamp");
+ Map<String, String> updatedUrls = new HashMap<String, String>();
+ JSONArray urls = object.getJSONArray("update_urls");
+ for (int i = 0; i < urls.length(); i++) {
+ JSONArray urlPair = urls.getJSONArray(i);
+ updatedUrls.put(urlPair.getString(0), urlPair.getString(1));
+ }
+ return new GpodnetUploadChangesResponse(timestamp, updatedUrls);
+ }
+
+ @Override
+ public String toString() {
+ return "GpodnetUploadChangesResponse{" +
+ "timestamp=" + timestamp +
+ ", updatedUrls=" + updatedUrls +
+ '}';
+ }
+}
diff --git a/src/de/danoeh/antennapod/miroguide/conn/MiroGuideConnector.java b/src/de/danoeh/antennapod/miroguide/conn/MiroGuideConnector.java
index 99bef4bd8..ee1a3ea21 100644
--- a/src/de/danoeh/antennapod/miroguide/conn/MiroGuideConnector.java
+++ b/src/de/danoeh/antennapod/miroguide/conn/MiroGuideConnector.java
@@ -16,6 +16,8 @@ import org.json.JSONObject;
import android.net.Uri;
+import de.danoeh.antennapod.util.LangUtils;
+
/** Executes HTTP requests and returns the results. */
public class MiroGuideConnector {
private HttpClient httpClient;
@@ -73,12 +75,14 @@ public class MiroGuideConnector {
if (response.getStatusLine().getStatusCode() == 200) {
HttpEntity entity = response.getEntity();
if (entity != null) {
- InputStream in = entity.getContent();
-
BufferedReader reader = new BufferedReader(
- new InputStreamReader(in));
- result = reader.readLine();
- in.close();
+ new InputStreamReader(entity.getContent(),
+ LangUtils.UTF_8));
+ try {
+ result = reader.readLine();
+ } finally {
+ reader.close();
+ }
}
} else {
throw new MiroGuideException(response.getStatusLine()
diff --git a/src/de/danoeh/antennapod/miroguide/model/MiroGuideItem.java b/src/de/danoeh/antennapod/miroguide/model/MiroGuideItem.java
index 89a2250df..cb5b15c56 100644
--- a/src/de/danoeh/antennapod/miroguide/model/MiroGuideItem.java
+++ b/src/de/danoeh/antennapod/miroguide/model/MiroGuideItem.java
@@ -12,7 +12,7 @@ public class MiroGuideItem {
super();
this.name = name;
this.description = description;
- this.date = date;
+ this.date = (Date) date.clone();
this.url = url;
}
@@ -30,7 +30,7 @@ public class MiroGuideItem {
}
public Date getDate() {
- return date;
+ return (Date) date.clone();
}
public String getUrl() {
diff --git a/src/de/danoeh/antennapod/preferences/GpodnetPreferences.java b/src/de/danoeh/antennapod/preferences/GpodnetPreferences.java
new file mode 100644
index 000000000..44b0f3cc3
--- /dev/null
+++ b/src/de/danoeh/antennapod/preferences/GpodnetPreferences.java
@@ -0,0 +1,217 @@
+package de.danoeh.antennapod.preferences;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.util.Log;
+import de.danoeh.antennapod.AppConfig;
+import de.danoeh.antennapod.PodcastApp;
+import de.danoeh.antennapod.service.GpodnetSyncService;
+
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.concurrent.locks.ReentrantLock;
+
+/**
+ * Manages preferences for accessing gpodder.net service
+ */
+public class GpodnetPreferences {
+
+ private static final String TAG = "GpodnetPreferences";
+
+ private static final String PREF_NAME = "gpodder.net";
+ public static final String PREF_GPODNET_USERNAME = "de.danoeh.antennapod.preferences.gpoddernet.username";
+ public static final String PREF_GPODNET_PASSWORD = "de.danoeh.antennapod.preferences.gpoddernet.password";
+ public static final String PREF_GPODNET_DEVICEID = "de.danoeh.antennapod.preferences.gpoddernet.deviceID";
+
+ public static final String PREF_LAST_SYNC_TIMESTAMP = "de.danoeh.antennapod.preferences.gpoddernet.last_sync_timestamp";
+ public static final String PREF_SYNC_ADDED = "de.danoeh.antennapod.preferences.gpoddernet.sync_added";
+ public static final String PREF_SYNC_REMOVED = "de.danoeh.antennapod.preferences.gpoddernet.sync_removed";
+
+ private static String username;
+ private static String password;
+ private static String deviceID;
+
+ private static ReentrantLock feedListLock = new ReentrantLock();
+ private static Set<String> addedFeeds;
+ private static Set<String> removedFeeds;
+
+ /**
+ * Last value returned by getSubscriptionChanges call. Will be used for all subsequent calls of getSubscriptionChanges.
+ */
+ private static long lastSyncTimestamp;
+
+ private static boolean preferencesLoaded = false;
+
+ private static SharedPreferences getPreferences() {
+ return PodcastApp.getInstance().getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE);
+ }
+
+ private static synchronized void ensurePreferencesLoaded() {
+ if (!preferencesLoaded) {
+ SharedPreferences prefs = getPreferences();
+ username = prefs.getString(PREF_GPODNET_USERNAME, null);
+ password = prefs.getString(PREF_GPODNET_PASSWORD, null);
+ deviceID = prefs.getString(PREF_GPODNET_DEVICEID, null);
+ lastSyncTimestamp = prefs.getLong(PREF_LAST_SYNC_TIMESTAMP, 0);
+ addedFeeds = readListFromString(prefs.getString(PREF_SYNC_ADDED, ""));
+ removedFeeds = readListFromString(prefs.getString(PREF_SYNC_REMOVED, ""));
+
+ preferencesLoaded = true;
+ }
+ }
+
+ private static void writePreference(String key, String value) {
+ SharedPreferences.Editor editor = getPreferences().edit();
+ editor.putString(key, value);
+ editor.commit();
+ }
+
+ private static void writePreference(String key, long value) {
+ SharedPreferences.Editor editor = getPreferences().edit();
+ editor.putLong(key, value);
+ editor.commit();
+ }
+
+ private static void writePreference(String key, Collection<String> value) {
+ SharedPreferences.Editor editor = getPreferences().edit();
+ editor.putString(key, writeListToString(value));
+ editor.commit();
+ }
+
+ public static String getUsername() {
+ ensurePreferencesLoaded();
+ return username;
+ }
+
+ public static void setUsername(String username) {
+ GpodnetPreferences.username = username;
+ writePreference(PREF_GPODNET_USERNAME, username);
+ }
+
+ public static String getPassword() {
+ ensurePreferencesLoaded();
+ return password;
+ }
+
+ public static void setPassword(String password) {
+ GpodnetPreferences.password = password;
+ writePreference(PREF_GPODNET_PASSWORD, password);
+ }
+
+ public static String getDeviceID() {
+ ensurePreferencesLoaded();
+ return deviceID;
+ }
+
+ public static void setDeviceID(String deviceID) {
+ GpodnetPreferences.deviceID = deviceID;
+ writePreference(PREF_GPODNET_DEVICEID, deviceID);
+ }
+
+ public static long getLastSyncTimestamp() {
+ ensurePreferencesLoaded();
+ return lastSyncTimestamp;
+ }
+
+ public static void setLastSyncTimestamp(long lastSyncTimestamp) {
+ GpodnetPreferences.lastSyncTimestamp = lastSyncTimestamp;
+ writePreference(PREF_LAST_SYNC_TIMESTAMP, lastSyncTimestamp);
+ }
+
+ public static void addAddedFeed(String feed) {
+ ensurePreferencesLoaded();
+ feedListLock.lock();
+ if (addedFeeds.add(feed)) {
+ writePreference(PREF_SYNC_ADDED, addedFeeds);
+ }
+ if (removedFeeds.remove(feed)) {
+ writePreference(PREF_SYNC_REMOVED, removedFeeds);
+ }
+ feedListLock.unlock();
+ GpodnetSyncService.sendSyncIntent(PodcastApp.getInstance());
+ }
+
+ public static void addRemovedFeed(String feed) {
+ ensurePreferencesLoaded();
+ feedListLock.lock();
+ if (removedFeeds.add(feed)) {
+ writePreference(PREF_SYNC_REMOVED, removedFeeds);
+ }
+ if (addedFeeds.remove(feed)) {
+ writePreference(PREF_SYNC_ADDED, addedFeeds);
+ }
+ feedListLock.unlock();
+ GpodnetSyncService.sendSyncIntent(PodcastApp.getInstance());
+ }
+
+ public static Set<String> getAddedFeedsCopy() {
+ ensurePreferencesLoaded();
+ Set<String> copy = new HashSet<String>();
+ feedListLock.lock();
+ copy.addAll(addedFeeds);
+ feedListLock.unlock();
+ return copy;
+ }
+
+ public static void removeAddedFeeds(Collection<String> removed) {
+ ensurePreferencesLoaded();
+ feedListLock.lock();
+ addedFeeds.removeAll(removed);
+ writePreference(PREF_SYNC_ADDED, addedFeeds);
+ feedListLock.unlock();
+ }
+
+ public static Set<String> getRemovedFeedsCopy() {
+ ensurePreferencesLoaded();
+ Set<String> copy = new HashSet<String>();
+ feedListLock.lock();
+ copy.addAll(removedFeeds);
+ feedListLock.unlock();
+ return copy;
+ }
+
+ public static void removeRemovedFeeds(Collection<String> removed) {
+ ensurePreferencesLoaded();
+ removedFeeds.removeAll(removed);
+ writePreference(PREF_SYNC_REMOVED, removedFeeds);
+
+ }
+
+ /**
+ * Returns true if device ID, username and password have a non-null value
+ */
+ public static boolean loggedIn() {
+ ensurePreferencesLoaded();
+ return deviceID != null && username != null && password != null;
+ }
+
+ public static synchronized void logout() {
+ if (AppConfig.DEBUG) Log.d(TAG, "Logout: Clearing preferences");
+ setUsername(null);
+ setPassword(null);
+ setDeviceID(null);
+ addedFeeds.clear();
+ writePreference(PREF_SYNC_ADDED, addedFeeds);
+ removedFeeds.clear();
+ writePreference(PREF_SYNC_REMOVED, removedFeeds);
+ setLastSyncTimestamp(0);
+ }
+
+ private static Set<String> readListFromString(String s) {
+ Set<String> result = new HashSet<String>();
+ for (String item : s.split(" ")) {
+ result.add(item);
+ }
+ return result;
+ }
+
+ private static String writeListToString(Collection<String> c) {
+ StringBuilder result = new StringBuilder();
+ for (String item : c) {
+ result.append(item);
+ result.append(" ");
+ }
+ return result.toString().trim();
+ }
+}
diff --git a/src/de/danoeh/antennapod/preferences/UserPreferences.java b/src/de/danoeh/antennapod/preferences/UserPreferences.java
index f2f35f41d..0d07a7178 100644
--- a/src/de/danoeh/antennapod/preferences/UserPreferences.java
+++ b/src/de/danoeh/antennapod/preferences/UserPreferences.java
@@ -2,9 +2,13 @@ package de.danoeh.antennapod.preferences;
import java.io.File;
import java.io.IOException;
+import java.util.LinkedList;
+import java.util.List;
import java.util.concurrent.TimeUnit;
import org.apache.commons.lang3.StringUtils;
+import org.json.JSONArray;
+import org.json.JSONException;
import android.app.AlarmManager;
import android.app.PendingIntent;
@@ -41,11 +45,13 @@ public class UserPreferences implements
public static final String PREF_ENABLE_AUTODL_WIFI_FILTER = "prefEnableAutoDownloadWifiFilter";
private static final String PREF_AUTODL_SELECTED_NETWORKS = "prefAutodownloadSelectedNetworks";
public static final String PREF_EPISODE_CACHE_SIZE = "prefEpisodeCacheSize";
+ private static final String PREF_PLAYBACK_SPEED = "prefPlaybackSpeed";
+ private static final String PREF_PLAYBACK_SPEED_ARRAY = "prefPlaybackSpeedArray";
private static int EPISODE_CACHE_SIZE_UNLIMITED = -1;
private static UserPreferences instance;
- private Context context;
+ private final Context context;
// Preferences
private boolean pauseOnHeadsetDisconnect;
@@ -60,6 +66,8 @@ public class UserPreferences implements
private boolean enableAutodownloadWifiFilter;
private String[] autodownloadSelectedNetworks;
private int episodeCacheSize;
+ private String playbackSpeed;
+ private String[] playbackSpeedArray;
private UserPreferences(Context context) {
this.context = context;
@@ -83,6 +91,7 @@ public class UserPreferences implements
createNoMediaFile();
PreferenceManager.getDefaultSharedPreferences(context)
.registerOnSharedPreferenceChangeListener(instance);
+
}
private void loadPreferences() {
@@ -108,6 +117,9 @@ public class UserPreferences implements
episodeCacheSize = readEpisodeCacheSize(sp.getString(
PREF_EPISODE_CACHE_SIZE, "20"));
enableAutodownload = sp.getBoolean(PREF_ENABLE_AUTODL, false);
+ playbackSpeed = sp.getString(PREF_PLAYBACK_SPEED, "1.0");
+ playbackSpeedArray = readPlaybackSpeedArray(sp.getString(
+ PREF_PLAYBACK_SPEED_ARRAY, null));
}
private int readThemeValue(String valueFromPrefs) {
@@ -135,6 +147,36 @@ public class UserPreferences implements
}
}
+ private String[] readPlaybackSpeedArray(String valueFromPrefs) {
+ String[] selectedSpeeds = null;
+ // If this preference hasn't been set yet, return the default options
+ if (valueFromPrefs == null) {
+ String[] allSpeeds = context.getResources().getStringArray(
+ R.array.playback_speed_values);
+ List<String> speedList = new LinkedList<String>();
+ for (String speedStr : allSpeeds) {
+ float speed = Float.parseFloat(speedStr);
+ if (speed < 2.0001 && speed * 10 % 1 == 0) {
+ speedList.add(speedStr);
+ }
+ }
+ selectedSpeeds = speedList.toArray(new String[speedList.size()]);
+ } else {
+ try {
+ JSONArray jsonArray = new JSONArray(valueFromPrefs);
+ selectedSpeeds = new String[jsonArray.length()];
+ for (int i = 0; i < jsonArray.length(); i++) {
+ selectedSpeeds[i] = jsonArray.getString(i);
+ }
+ } catch (JSONException e) {
+ Log.e(TAG,
+ "Got JSON error when trying to get speeds from JSONArray");
+ e.printStackTrace();
+ }
+ }
+ return selectedSpeeds;
+ }
+
private static void instanceAvailable() {
if (instance == null) {
throw new IllegalStateException(
@@ -169,7 +211,8 @@ public class UserPreferences implements
public static boolean isDisplayOnlyEpisodes() {
instanceAvailable();
- return instance.displayOnlyEpisodes;
+ //return instance.displayOnlyEpisodes;
+ return false;
}
public static boolean isAutoDelete() {
@@ -196,6 +239,16 @@ public class UserPreferences implements
return EPISODE_CACHE_SIZE_UNLIMITED;
}
+ public static String getPlaybackSpeed() {
+ instanceAvailable();
+ return instance.playbackSpeed;
+ }
+
+ public static String[] getPlaybackSpeedArray() {
+ instanceAvailable();
+ return instance.playbackSpeedArray;
+ }
+
/**
* Returns the capacity of the episode cache. This method will return the
* negative integer EPISODE_CACHE_SIZE_UNLIMITED if the cache size is set to
@@ -250,7 +303,27 @@ public class UserPreferences implements
PREF_EPISODE_CACHE_SIZE, "20"));
} else if (key.equals(PREF_ENABLE_AUTODL)) {
enableAutodownload = sp.getBoolean(PREF_ENABLE_AUTODL, false);
+ } else if (key.equals(PREF_PLAYBACK_SPEED)) {
+ playbackSpeed = sp.getString(PREF_PLAYBACK_SPEED, "1.0");
+ } else if (key.equals(PREF_PLAYBACK_SPEED_ARRAY)) {
+ playbackSpeedArray = readPlaybackSpeedArray(sp.getString(
+ PREF_PLAYBACK_SPEED_ARRAY, null));
+ }
+ }
+
+ public static void setPlaybackSpeed(String speed) {
+ PreferenceManager.getDefaultSharedPreferences(instance.context).edit()
+ .putString(PREF_PLAYBACK_SPEED, speed).apply();
+ }
+
+ public static void setPlaybackSpeedArray(String[] speeds) {
+ JSONArray jsonArray = new JSONArray();
+ for (String speed : speeds) {
+ jsonArray.put(speed);
}
+ PreferenceManager.getDefaultSharedPreferences(instance.context).edit()
+ .putString(PREF_PLAYBACK_SPEED_ARRAY, jsonArray.toString())
+ .apply();
}
public static void setAutodownloadSelectedNetworks(Context context,
diff --git a/src/de/danoeh/antennapod/service/GpodnetSyncService.java b/src/de/danoeh/antennapod/service/GpodnetSyncService.java
new file mode 100644
index 000000000..71e128b55
--- /dev/null
+++ b/src/de/danoeh/antennapod/service/GpodnetSyncService.java
@@ -0,0 +1,243 @@
+package de.danoeh.antennapod.service;
+
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.Service;
+import android.content.Context;
+import android.content.Intent;
+import android.os.IBinder;
+import android.support.v4.app.NotificationCompat;
+import android.util.Log;
+import de.danoeh.antennapod.AppConfig;
+import de.danoeh.antennapod.R;
+import de.danoeh.antennapod.feed.Feed;
+import de.danoeh.antennapod.gpoddernet.GpodnetService;
+import de.danoeh.antennapod.gpoddernet.GpodnetServiceAuthenticationException;
+import de.danoeh.antennapod.gpoddernet.GpodnetServiceException;
+import de.danoeh.antennapod.gpoddernet.model.GpodnetSubscriptionChange;
+import de.danoeh.antennapod.gpoddernet.model.GpodnetUploadChangesResponse;
+import de.danoeh.antennapod.preferences.GpodnetPreferences;
+import de.danoeh.antennapod.storage.*;
+import de.danoeh.antennapod.util.NetworkUtils;
+
+import java.util.Date;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.ExecutionException;
+
+/**
+ * Synchronizes local subscriptions with gpodder.net service. The service should be started with ACTION_SYNC as an action argument.
+ * This class also provides static methods for starting the GpodnetSyncService.
+ */
+public class GpodnetSyncService extends Service {
+ private static final String TAG = "GpodnetSyncService";
+
+ private static final long WAIT_INTERVAL = 5000L;
+
+ public static final String ARG_ACTION = "action";
+
+ public static final String ACTION_SYNC = "de.danoeh.antennapod.intent.action.sync";
+
+ private GpodnetService service;
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ final String action = (intent != null) ? intent.getStringExtra(ARG_ACTION) : null;
+ if (action != null && action.equals(ACTION_SYNC)) {
+ Log.d(TAG, String.format("Waiting %d milliseconds before uploading changes", WAIT_INTERVAL));
+ syncWaiterThread.restart();
+ } else {
+ Log.e(TAG, "Received invalid intent: action argument is null or invalid");
+ }
+ return START_FLAG_REDELIVERY;
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ if (AppConfig.DEBUG) Log.d(TAG, "onDestroy");
+ syncWaiterThread.interrupt();
+
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return null;
+ }
+
+ private synchronized GpodnetService tryLogin() throws GpodnetServiceException {
+ if (service == null) {
+ service = new GpodnetService();
+ service.authenticate(GpodnetPreferences.getUsername(), GpodnetPreferences.getPassword());
+ }
+ return service;
+ }
+
+ private synchronized void syncChanges() {
+ if (GpodnetPreferences.loggedIn() && NetworkUtils.networkAvailable(this)) {
+ final long timestamp = GpodnetPreferences.getLastSyncTimestamp();
+ try {
+ final List<String> localSubscriptions = DBReader.getFeedListDownloadUrls(this);
+ GpodnetService service = tryLogin();
+
+ if (timestamp == 0) {
+ // first sync: download all subscriptions...
+ GpodnetSubscriptionChange changes =
+ service.getSubscriptionChanges(GpodnetPreferences.getUsername(), GpodnetPreferences.getDeviceID(), 0);
+ if (AppConfig.DEBUG) Log.d(TAG, "Downloaded subscription changes: " + changes);
+ processSubscriptionChanges(localSubscriptions, changes);
+
+ // ... then upload all local subscriptions
+ if (AppConfig.DEBUG) Log.d(TAG, "Uploading subscription list: " + localSubscriptions);
+ GpodnetUploadChangesResponse uploadChangesResponse =
+ service.uploadChanges(GpodnetPreferences.getUsername(), GpodnetPreferences.getDeviceID(), localSubscriptions, new LinkedList<String>());
+ if (AppConfig.DEBUG) Log.d(TAG, "Uploading changes response: " + uploadChangesResponse);
+ DBWriter.updateFeedDownloadURLs(GpodnetSyncService.this, uploadChangesResponse.updatedUrls).get();
+ GpodnetPreferences.removeAddedFeeds(localSubscriptions);
+ GpodnetPreferences.removeRemovedFeeds(GpodnetPreferences.getRemovedFeedsCopy());
+ GpodnetPreferences.setLastSyncTimestamp(uploadChangesResponse.timestamp);
+ } else {
+ Set<String> added = GpodnetPreferences.getAddedFeedsCopy();
+ Set<String> removed = GpodnetPreferences.getRemovedFeedsCopy();
+
+ // download remote changes first...
+ GpodnetSubscriptionChange subscriptionChanges = service.getSubscriptionChanges(GpodnetPreferences.getUsername(), GpodnetPreferences.getDeviceID(), timestamp);
+ if (AppConfig.DEBUG) Log.d(TAG, "Downloaded subscription changes: " + subscriptionChanges);
+ processSubscriptionChanges(localSubscriptions, subscriptionChanges);
+
+ // ... then upload changes local changes
+ if (AppConfig.DEBUG) Log.d(TAG, String.format("Uploading subscriptions, Added: %s\nRemoved: %s",
+ added.toString(), removed));
+ GpodnetUploadChangesResponse uploadChangesResponse = service.uploadChanges(GpodnetPreferences.getUsername(), GpodnetPreferences.getDeviceID(), added, removed);
+ if (AppConfig.DEBUG) Log.d(TAG, "Upload subscriptions response: " + uploadChangesResponse);
+
+ GpodnetPreferences.removeAddedFeeds(added);
+ GpodnetPreferences.removeRemovedFeeds(removed);
+ DBWriter.updateFeedDownloadURLs(GpodnetSyncService.this, uploadChangesResponse.updatedUrls).get();
+ GpodnetPreferences.setLastSyncTimestamp(uploadChangesResponse.timestamp);
+ }
+ clearErrorNotifications();
+ } catch (GpodnetServiceException e) {
+ e.printStackTrace();
+ updateErrorNotification(e);
+ } catch (DownloadRequestException e) {
+ e.printStackTrace();
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ } catch (ExecutionException e) {
+ e.printStackTrace();
+ }
+ }
+ stopSelf();
+ }
+
+ private synchronized void processSubscriptionChanges(List<String> localSubscriptions, GpodnetSubscriptionChange changes) throws DownloadRequestException {
+ for (String downloadUrl : changes.getAdded()) {
+ if (!localSubscriptions.contains(downloadUrl)) {
+ Feed feed = new Feed(downloadUrl, new Date());
+ DownloadRequester.getInstance().downloadFeed(this, feed);
+ }
+ }
+ for (String downloadUrl : changes.getRemoved()) {
+ DBTasks.removeFeedWithDownloadUrl(GpodnetSyncService.this, downloadUrl);
+ }
+ }
+
+ private void clearErrorNotifications() {
+ NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
+ nm.cancel(R.id.notification_gpodnet_sync_error);
+ nm.cancel(R.id.notification_gpodnet_sync_autherror);
+ }
+
+ private void updateErrorNotification(GpodnetServiceException exception) {
+ if (AppConfig.DEBUG) Log.d(TAG, "Posting error notification");
+
+ NotificationCompat.Builder builder = new NotificationCompat.Builder(this);
+ final String title;
+ final String description;
+ final int id;
+ if (exception instanceof GpodnetServiceAuthenticationException) {
+ title = getString(R.string.gpodnetsync_auth_error_title);
+ description = getString(R.string.gpodnetsync_auth_error_descr);
+ id = R.id.notification_gpodnet_sync_autherror;
+ } else {
+ title = getString(R.string.gpodnetsync_error_title);
+ description = getString(R.string.gpodnetsync_error_descr) + exception.getMessage();
+ id = R.id.notification_gpodnet_sync_error;
+ }
+ Notification notification = builder.setContentTitle(title)
+ .setContentText(description)
+ .setSmallIcon(R.drawable.stat_notify_sync_error)
+ .setAutoCancel(true)
+ .build();
+ NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
+ nm.notify(id, notification);
+ }
+
+ private WaiterThread syncWaiterThread = new WaiterThread(WAIT_INTERVAL) {
+ @Override
+ public void onWaitCompleted() {
+ syncChanges();
+ }
+ };
+
+ private abstract class WaiterThread {
+ private long waitInterval;
+ private Thread thread;
+
+ private WaiterThread(long waitInterval) {
+ this.waitInterval = waitInterval;
+ reinit();
+ }
+
+ public abstract void onWaitCompleted();
+
+ public void exec() {
+ if (!thread.isAlive()) {
+ thread.start();
+ }
+ }
+
+ private void reinit() {
+ if (thread != null && thread.isAlive()) {
+ Log.d(TAG, "Interrupting waiter thread");
+ thread.interrupt();
+ }
+ thread = new Thread() {
+ @Override
+ public void run() {
+ try {
+ Thread.sleep(waitInterval);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ if (!isInterrupted()) {
+ synchronized (this) {
+ onWaitCompleted();
+ }
+ }
+ }
+ };
+ }
+
+ public void restart() {
+ reinit();
+ exec();
+ }
+
+ public void interrupt() {
+ if (thread != null && thread.isAlive()) {
+ thread.interrupt();
+ }
+ }
+ }
+
+ public static void sendSyncIntent(Context context) {
+ if (GpodnetPreferences.loggedIn()) {
+ Intent intent = new Intent(context, GpodnetSyncService.class);
+ intent.putExtra(ARG_ACTION, ACTION_SYNC);
+ context.startService(intent);
+ }
+ }
+}
diff --git a/src/de/danoeh/antennapod/service/PlaybackService.java b/src/de/danoeh/antennapod/service/PlaybackService.java
index 7c306984c..556a58ee8 100644
--- a/src/de/danoeh/antennapod/service/PlaybackService.java
+++ b/src/de/danoeh/antennapod/service/PlaybackService.java
@@ -45,9 +45,13 @@ 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;
/**
@@ -119,7 +123,12 @@ public class PlaybackService extends Service {
*/
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.
*/
@@ -132,13 +141,12 @@ public class PlaybackService extends Service {
private static final int NOTIFICATION_ID = 1;
+ private volatile IPlayer player;
+ private RemoteControlClient remoteControlClient;
private AudioManager audioManager;
private ComponentName mediaButtonReceiver;
- private MediaPlayer player;
- private RemoteControlClient remoteControlClient;
-
- private Playable media;
+ private volatile Playable media;
/**
* True if media should be streamed (Extracted from Intent Extra) .
@@ -252,7 +260,6 @@ public class PlaybackService extends Service {
}
);
dbLoaderExecutor = Executors.newSingleThreadExecutor();
- player = createMediaPlayer();
mediaButtonReceiver = new ComponentName(getPackageName(),
MediaButtonReceiver.class.getName());
@@ -273,22 +280,43 @@ public class PlaybackService extends Service {
loadQueue();
}
- private MediaPlayer createMediaPlayer() {
- return createMediaPlayer(new MediaPlayer());
- }
-
- private MediaPlayer createMediaPlayer(MediaPlayer mp) {
- if (mp != null) {
- mp.setOnPreparedListener(preparedListener);
- mp.setOnCompletionListener(completionListener);
- mp.setOnSeekCompleteListener(onSeekCompleteListener);
- mp.setOnErrorListener(onErrorListener);
- mp.setOnBufferingUpdateListener(onBufferingUpdateListener);
- mp.setOnInfoListener(onInfoListener);
+ private IPlayer createMediaPlayer() {
+ IPlayer player;
+ if (media == null || media.getMediaType() == MediaType.VIDEO) {
+ player = new VideoPlayer();
+ } else {
+ player = new AudioPlayer(this);
}
- return mp;
+ 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() {
@@ -475,7 +503,7 @@ public class PlaybackService extends Service {
seekDelta(-PlaybackController.DEFAULT_SEEK_DELTA);
break;
}
- }
+ }
}
/**
@@ -568,6 +596,7 @@ public class PlaybackService extends Service {
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");
@@ -662,105 +691,169 @@ public class PlaybackService extends Service {
}
}
- private MediaPlayer.OnPreparedListener preparedListener = new MediaPlayer.OnPreparedListener() {
- @Override
- public void onPrepared(MediaPlayer mp) {
- 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();
+ 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);
+ }
+ };
- if (startWhenPrepared) {
- play();
- }
- }
- };
+ 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();
- private MediaPlayer.OnSeekCompleteListener onSeekCompleteListener = new MediaPlayer.OnSeekCompleteListener() {
+ if (startWhenPrepared) {
+ play();
+ }
+ }
- @Override
- public void onSeekComplete(MediaPlayer mp) {
- if (status == PlayerStatus.SEEKING) {
- setStatus(statusBeforeSeek);
- }
+ 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 MediaPlayer.OnInfoListener onInfoListener = new MediaPlayer.OnInfoListener() {
+ private final void genericSeekCompleteListener() {
+ if (status == PlayerStatus.SEEKING) {
+ setStatus(statusBeforeSeek);
+ }
+ }
- @Override
- public boolean onInfo(MediaPlayer mp, int what, int extra) {
- 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.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 MediaPlayer.OnErrorListener onErrorListener = new MediaPlayer.OnErrorListener() {
- private static final String TAG = "PlaybackService.onErrorListener";
+ 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);
+ }
+ };
- @Override
- public boolean onError(MediaPlayer mp, int what, int extra) {
- Log.w(TAG, "An error has occured: " + what);
- if (mp.isPlaying()) {
- pause(true, true);
- }
- sendNotificationBroadcast(NOTIFICATION_TYPE_ERROR, what);
- setCurrentlyPlayingMedia(PlaybackPreferences.NO_MEDIA_PLAYING);
- stopSelf();
- return true;
- }
- };
+ 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 MediaPlayer.OnCompletionListener completionListener = new MediaPlayer.OnCompletionListener() {
+ 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);
+ }
+ };
- @Override
- public void onCompletion(MediaPlayer mp) {
- endPlayback(true);
- }
- };
+ 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 MediaPlayer.OnBufferingUpdateListener onBufferingUpdateListener = new MediaPlayer.OnBufferingUpdateListener() {
+ 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;
+ }
- @Override
- public void onBufferingUpdate(MediaPlayer mp, int percent) {
- sendNotificationBroadcast(NOTIFICATION_TYPE_BUFFER_UPDATE, percent);
+ 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)
@@ -783,7 +876,6 @@ public class PlaybackService extends Service {
DBWriter.removeQueueItem(PlaybackService.this, item.getId(), true);
}
DBWriter.addItemToPlaybackHistory(PlaybackService.this, (FeedMedia) media);
- DBWriter.setFeedMedia(PlaybackService.this, (FeedMedia) media);
long autoDeleteMediaId = ((FeedComponent) media).getId();
if (shouldStream) {
autoDeleteMediaId = -1;
@@ -863,7 +955,7 @@ public class PlaybackService extends Service {
/**
* 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
@@ -939,6 +1031,7 @@ public class PlaybackService extends Service {
Log.d(TAG, "Resuming/Starting playback");
writePlaybackPreferences();
+ setSpeed(Float.parseFloat(UserPreferences.getPlaybackSpeed()));
player.start();
if (status != PlayerStatus.PAUSED) {
player.seekTo((int) media.getPosition());
@@ -1124,7 +1217,7 @@ public class PlaybackService extends Service {
/**
* Seek a specific position from the current position
- *
+ *
* @param delta
* offset from current position (positive or negative)
* */
@@ -1282,18 +1375,20 @@ public class PlaybackService extends Service {
isPlaying = true;
}
- 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());
+ 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);
}
- i.putExtra("duration", media.getDuration());
- i.putExtra("position", media.getPosition());
- sendBroadcast(i);
}
/**
@@ -1370,7 +1465,7 @@ public class PlaybackService extends Service {
}
}
}
- };
+ };
/** Periodically saves the position of the media file */
class PositionSaver implements Runnable {
@@ -1472,7 +1567,7 @@ public class PlaybackService extends Service {
return media;
}
- public MediaPlayer getPlayer() {
+ public IPlayer getPlayer() {
return player;
}
@@ -1485,6 +1580,53 @@ public class PlaybackService extends Service {
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
diff --git a/src/de/danoeh/antennapod/service/download/DownloadService.java b/src/de/danoeh/antennapod/service/download/DownloadService.java
index c84a6f913..4040c85a8 100644
--- a/src/de/danoeh/antennapod/service/download/DownloadService.java
+++ b/src/de/danoeh/antennapod/service/download/DownloadService.java
@@ -184,7 +184,7 @@ public class DownloadService extends Service {
public int onStartCommand(Intent intent, int flags, int startId) {
if (intent.getParcelableExtra(EXTRA_REQUEST) != null) {
onDownloadQueued(intent);
- } else if (numberOfDownloads.equals(0)) {
+ } else if (numberOfDownloads.get() == 0) {
stopSelf();
}
return Service.START_NOT_STICKY;
@@ -421,52 +421,24 @@ public class DownloadService extends Service {
return null;
}
- @SuppressLint("NewApi")
- public void onDownloadCompleted(final Downloader downloader) {
- final AsyncTask<Void, Void, Void> handlerTask = new AsyncTask<Void, Void, Void>() {
- boolean successful;
-
- @Override
- protected void onPostExecute(Void result) {
- super.onPostExecute(result);
- if (!successful) {
- queryDownloads();
- }
- }
-
- @Override
- protected void onPreExecute() {
- super.onPreExecute();
- removeDownload(downloader);
- }
-
- @Override
- protected Void doInBackground(Void... params) {
-
-
- return null;
- }
- };
- if (android.os.Build.VERSION.SDK_INT > android.os.Build.VERSION_CODES.GINGERBREAD_MR1) {
- handlerTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
- } else {
- handlerTask.execute();
- }
- }
-
/**
* Remove download from the DownloadRequester list and from the
* DownloadService list.
*/
private void removeDownload(final Downloader d) {
- if (AppConfig.DEBUG)
- Log.d(TAG, "Removing downloader: "
- + d.getDownloadRequest().getSource());
- boolean rc = downloads.remove(d);
- if (AppConfig.DEBUG)
- Log.d(TAG, "Result of downloads.remove: " + rc);
- DownloadRequester.getInstance().removeDownload(d.getDownloadRequest());
- sendBroadcast(new Intent(ACTION_DOWNLOADS_CONTENT_CHANGED));
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Removing downloader: "
+ + d.getDownloadRequest().getSource());
+ boolean rc = downloads.remove(d);
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Result of downloads.remove: " + rc);
+ DownloadRequester.getInstance().removeDownload(d.getDownloadRequest());
+ sendBroadcast(new Intent(ACTION_DOWNLOADS_CONTENT_CHANGED));
+ }
+ });
}
/**
@@ -828,8 +800,9 @@ public class DownloadService extends Service {
media.setFile_url(request.getDestination());
// Get duration
- MediaPlayer mediaplayer = new MediaPlayer();
+ MediaPlayer mediaplayer = null;
try {
+ mediaplayer = new MediaPlayer();
mediaplayer.setDataSource(media.getFile_url());
mediaplayer.prepare();
media.setDuration(mediaplayer.getDuration());
@@ -838,8 +811,13 @@ public class DownloadService extends Service {
mediaplayer.reset();
} catch (IOException e) {
e.printStackTrace();
+ } catch (RuntimeException e) {
+ // Thrown by MediaPlayer initialization on some devices
+ e.printStackTrace();
} finally {
- mediaplayer.release();
+ if (mediaplayer != null) {
+ mediaplayer.release();
+ }
}
if (media.getItem().getChapters() == null) {
diff --git a/src/de/danoeh/antennapod/service/download/DownloadStatus.java b/src/de/danoeh/antennapod/service/download/DownloadStatus.java
index 62e54cbb4..487c3b3de 100644
--- a/src/de/danoeh/antennapod/service/download/DownloadStatus.java
+++ b/src/de/danoeh/antennapod/service/download/DownloadStatus.java
@@ -52,7 +52,7 @@ public class DownloadStatus {
this.feedfileId = feedfileId;
this.reason = reason;
this.successful = successful;
- this.completionDate = completionDate;
+ this.completionDate = (Date) completionDate.clone();
this.reasonDetailed = reasonDetailed;
this.feedfileType = feedfileType;
}
@@ -133,7 +133,7 @@ public class DownloadStatus {
}
public Date getCompletionDate() {
- return completionDate;
+ return (Date) completionDate.clone();
}
public long getFeedfileId() {
@@ -162,6 +162,7 @@ public class DownloadStatus {
this.successful = false;
this.reason = reason;
this.reasonDetailed = reasonDetailed;
+ this.done = true;
}
public void setCancelled() {
@@ -172,7 +173,7 @@ public class DownloadStatus {
}
public void setCompletionDate(Date completionDate) {
- this.completionDate = completionDate;
+ this.completionDate = (Date) completionDate.clone();
}
public void setId(long id) {
diff --git a/src/de/danoeh/antennapod/service/download/HttpDownloader.java b/src/de/danoeh/antennapod/service/download/HttpDownloader.java
index c9671ceb3..582fb9575 100644
--- a/src/de/danoeh/antennapod/service/download/HttpDownloader.java
+++ b/src/de/danoeh/antennapod/service/download/HttpDownloader.java
@@ -6,12 +6,12 @@ import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
-import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.SocketTimeoutException;
import java.net.UnknownHostException;
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;
@@ -30,161 +30,186 @@ import de.danoeh.antennapod.util.DownloadError;
import de.danoeh.antennapod.util.StorageUtils;
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;
- }
-
- @Override
- protected void download() {
- DefaultHttpClient httpClient = null;
- OutputStream out = null;
- InputStream connection = null;
- try {
- HttpGet httpGet = new HttpGet(request.getSource());
- httpClient = createHttpClient();
- HttpResponse response = httpClient.execute(httpGet);
- HttpEntity httpEntity = response.getEntity();
- int responseCode = response.getStatusLine().getStatusCode();
- if (AppConfig.DEBUG)
- Log.d(TAG, "Response code is " + responseCode);
- if (responseCode == HttpURLConnection.HTTP_OK && httpEntity != null) {
- if (StorageUtils.storageAvailable(PodcastApp.getInstance())) {
- File destination = new File(request.getDestination());
- if (!destination.exists()) {
- connection = AndroidHttpClient
- .getUngzippedContent(httpEntity);
- InputStream in = new BufferedInputStream(connection);
- out = new BufferedOutputStream(new FileOutputStream(
- destination));
- byte[] buffer = new byte[BUFFER_SIZE];
- int count = 0;
- request.setStatusMsg(R.string.download_running);
- if (AppConfig.DEBUG)
- Log.d(TAG, "Getting size of download");
- request.setSize(httpEntity.getContentLength());
- if (AppConfig.DEBUG)
- Log.d(TAG, "Size is " + request.getSize());
- if (request.getSize() < 0) {
- request.setSize(DownloadStatus.SIZE_UNKNOWN);
- }
-
- long freeSpace = StorageUtils.getFreeSpaceAvailable();
- if (AppConfig.DEBUG)
- Log.d(TAG, "Free space is " + freeSpace);
- if (request.getSize() == DownloadStatus.SIZE_UNKNOWN
- || request.getSize() <= freeSpace) {
- if (AppConfig.DEBUG)
- Log.d(TAG, "Starting download");
- while (!cancelled
- && (count = in.read(buffer)) != -1) {
- out.write(buffer, 0, count);
- request.setSoFar(request.getSoFar() + count);
- request.setProgressPercent((int) (((double) request
- .getSoFar() / (double) request
- .getSize()) * 100));
- }
- if (cancelled) {
- onCancelled();
- } else {
- onSuccess();
- }
- } else {
- onFail(DownloadError.ERROR_NOT_ENOUGH_SPACE, null);
- }
- } else {
- Log.w(TAG, "File already exists");
- onFail(DownloadError.ERROR_FILE_EXISTS, null);
- }
- } else {
- onFail(DownloadError.ERROR_DEVICE_NOT_FOUND, null);
- }
- } else {
- onFail(DownloadError.ERROR_HTTP_DATA_ERROR,
- String.valueOf(responseCode));
- }
- } catch (IllegalArgumentException e) {
- e.printStackTrace();
- onFail(DownloadError.ERROR_MALFORMED_URL, e.getMessage());
- } catch (SocketTimeoutException e) {
- e.printStackTrace();
- onFail(DownloadError.ERROR_CONNECTION_ERROR, e.getMessage());
- } catch (UnknownHostException e) {
- e.printStackTrace();
- onFail(DownloadError.ERROR_UNKNOWN_HOST, e.getMessage());
- } catch (IOException e) {
- e.printStackTrace();
- onFail(DownloadError.ERROR_IO_ERROR, e.getMessage());
- } catch (NullPointerException e) {
- // might be thrown by connection.getInputStream()
- e.printStackTrace();
- onFail(DownloadError.ERROR_CONNECTION_ERROR, request.getSource());
- } finally {
- IOUtils.closeQuietly(out);
- if (httpClient != null) {
- httpClient.getConnectionManager().shutdown();
- }
- }
- }
-
- private void onSuccess() {
- if (AppConfig.DEBUG)
- Log.d(TAG, "Download was successful");
- result.setSuccessful();
- }
-
- private void onFail(DownloadError reason, String reasonDetailed) {
- if (AppConfig.DEBUG) {
- Log.d(TAG, "Download failed");
- }
+ 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;
+ }
+
+ @Override
+ protected void download() {
+ DefaultHttpClient httpClient = null;
+ BufferedOutputStream out = null;
+ InputStream connection = null;
+ try {
+ HttpGet httpGet = new HttpGet(request.getSource());
+ httpClient = createHttpClient();
+ HttpResponse response = httpClient.execute(httpGet);
+ HttpEntity httpEntity = response.getEntity();
+ int responseCode = response.getStatusLine().getStatusCode();
+ Header contentEncodingHeader = response.getFirstHeader("Content-Encoding");
+
+ final boolean isGzip = contentEncodingHeader != null &&
+ contentEncodingHeader.getValue().equalsIgnoreCase("gzip");
+
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Response code is " + responseCode);
+
+ if (responseCode != HttpURLConnection.HTTP_OK || httpEntity == null) {
+ onFail(DownloadError.ERROR_HTTP_DATA_ERROR,
+ String.valueOf(responseCode));
+ return;
+ }
+
+ if (!StorageUtils.storageAvailable(PodcastApp.getInstance())) {
+ onFail(DownloadError.ERROR_DEVICE_NOT_FOUND, null);
+ return;
+ }
+
+ File destination = new File(request.getDestination());
+ if (destination.exists()) {
+ Log.w(TAG, "File already exists");
+ onFail(DownloadError.ERROR_FILE_EXISTS, null);
+ return;
+ }
+
+ connection = new BufferedInputStream(AndroidHttpClient
+ .getUngzippedContent(httpEntity));
+ out = new BufferedOutputStream(new FileOutputStream(
+ destination));
+ byte[] buffer = new byte[BUFFER_SIZE];
+ int count = 0;
+ request.setStatusMsg(R.string.download_running);
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Getting size of download");
+ request.setSize(httpEntity.getContentLength());
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Size is " + request.getSize());
+ if (request.getSize() < 0) {
+ request.setSize(DownloadStatus.SIZE_UNKNOWN);
+ }
+
+ long freeSpace = StorageUtils.getFreeSpaceAvailable();
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Free space is " + freeSpace);
+
+ if (request.getSize() != DownloadStatus.SIZE_UNKNOWN
+ && request.getSize() > freeSpace) {
+ onFail(DownloadError.ERROR_NOT_ENOUGH_SPACE, null);
+ return;
+ }
+
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Starting download");
+ while (!cancelled
+ && (count = connection.read(buffer)) != -1) {
+ out.write(buffer, 0, count);
+ request.setSoFar(request.getSoFar() + count);
+ request.setProgressPercent((int) (((double) request
+ .getSoFar() / (double) request
+ .getSize()) * 100));
+ }
+ if (cancelled) {
+ onCancelled();
+ } else {
+ out.flush();
+ // check if size specified in the response header is the same as the size of the
+ // written file. This check cannot be made if compression was used
+ if (!isGzip && request.getSize() != DownloadStatus.SIZE_UNKNOWN &&
+ request.getSoFar() != request.getSize()) {
+ onFail(DownloadError.ERROR_IO_ERROR,
+ "Download completed but size: " +
+ request.getSoFar() +
+ " does not equal expected size " +
+ request.getSize());
+ return;
+ }
+ onSuccess();
+ }
+
+ } catch (IllegalArgumentException e) {
+ e.printStackTrace();
+ onFail(DownloadError.ERROR_MALFORMED_URL, e.getMessage());
+ } catch (SocketTimeoutException e) {
+ e.printStackTrace();
+ onFail(DownloadError.ERROR_CONNECTION_ERROR, e.getMessage());
+ } catch (UnknownHostException e) {
+ e.printStackTrace();
+ onFail(DownloadError.ERROR_UNKNOWN_HOST, e.getMessage());
+ } catch (IOException e) {
+ e.printStackTrace();
+ onFail(DownloadError.ERROR_IO_ERROR, e.getMessage());
+ } catch (NullPointerException e) {
+ // might be thrown by connection.getInputStream()
+ e.printStackTrace();
+ onFail(DownloadError.ERROR_CONNECTION_ERROR, request.getSource());
+ } finally {
+ IOUtils.closeQuietly(out);
+ if (httpClient != null) {
+ httpClient.getConnectionManager().shutdown();
+ }
+ }
+ }
+
+ private void onSuccess() {
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Download was successful");
+ result.setSuccessful();
+ }
+
+ private void onFail(DownloadError reason, String reasonDetailed) {
+ if (AppConfig.DEBUG) {
+ Log.d(TAG, "Download failed");
+ }
result.setFailed(reason, reasonDetailed);
- cleanup();
- }
+ cleanup();
+ }
- private void onCancelled() {
- if (AppConfig.DEBUG)
- Log.d(TAG, "Download was cancelled");
+ private void onCancelled() {
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Download was cancelled");
result.setCancelled();
- cleanup();
- }
-
- /** Deletes unfinished downloads. */
- private void cleanup() {
- if (request.getDestination() != null) {
- File dest = new File(request.getDestination());
- if (dest.exists()) {
- boolean rc = dest.delete();
- if (AppConfig.DEBUG)
- Log.d(TAG, "Deleted file " + dest.getName() + "; Result: "
- + rc);
- } else {
- if (AppConfig.DEBUG)
- Log.d(TAG, "cleanup() didn't delete file: does not exist.");
- }
- }
- }
+ cleanup();
+ }
+
+ /**
+ * Deletes unfinished downloads.
+ */
+ private void cleanup() {
+ if (request.getDestination() != null) {
+ File dest = new File(request.getDestination());
+ if (dest.exists()) {
+ boolean rc = dest.delete();
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Deleted file " + dest.getName() + "; Result: "
+ + rc);
+ } else {
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "cleanup() didn't delete file: does not exist.");
+ }
+ }
+ }
}
diff --git a/src/de/danoeh/antennapod/storage/DBReader.java b/src/de/danoeh/antennapod/storage/DBReader.java
index c96051874..a5a4c8cd4 100644
--- a/src/de/danoeh/antennapod/storage/DBReader.java
+++ b/src/de/danoeh/antennapod/storage/DBReader.java
@@ -76,6 +76,27 @@ public final class DBReader {
}
/**
+ * Returns a list with the download URLs of all feeds.
+ * @param context A context that is used for opening the database connection.
+ * @return A list of Strings with the download URLs of all feeds.
+ * */
+ public static List<String> getFeedListDownloadUrls(final Context context) {
+ PodDBAdapter adapter = new PodDBAdapter(context);
+ List<String> result = new ArrayList<String>();
+ adapter.open();
+ Cursor feeds = adapter.getFeedCursorDownloadUrls();
+ if (feeds.moveToFirst()) {
+ do {
+ result.add(feeds.getString(1));
+ } while (feeds.moveToNext());
+ }
+ feeds.close();
+ adapter.close();
+
+ return result;
+ }
+
+ /**
* Returns a list of 'expired Feeds', i.e. Feeds that have not been updated for a certain amount of time.
*
* @param context A context that is used for opening a database connection.
@@ -229,9 +250,11 @@ public final class DBReader {
title, item, link);
break;
}
- chapter.setId(chapterCursor
- .getLong(PodDBAdapter.KEY_ID_INDEX));
- item.getChapters().add(chapter);
+ if (chapter != null) {
+ chapter.setId(chapterCursor
+ .getLong(PodDBAdapter.KEY_ID_INDEX));
+ item.getChapters().add(chapter);
+ }
} while (chapterCursor.moveToNext());
}
chapterCursor.close();
diff --git a/src/de/danoeh/antennapod/storage/DBTasks.java b/src/de/danoeh/antennapod/storage/DBTasks.java
index b1efda658..b9a1fd002 100644
--- a/src/de/danoeh/antennapod/storage/DBTasks.java
+++ b/src/de/danoeh/antennapod/storage/DBTasks.java
@@ -23,11 +23,13 @@ import de.danoeh.antennapod.feed.FeedImage;
import de.danoeh.antennapod.feed.FeedItem;
import de.danoeh.antennapod.feed.FeedMedia;
import de.danoeh.antennapod.preferences.UserPreferences;
+import de.danoeh.antennapod.service.GpodnetSyncService;
import de.danoeh.antennapod.service.PlaybackService;
import de.danoeh.antennapod.service.download.DownloadStatus;
import de.danoeh.antennapod.util.DownloadError;
import de.danoeh.antennapod.util.NetworkUtils;
import de.danoeh.antennapod.util.QueueAccess;
+import de.danoeh.antennapod.util.comparator.FeedItemPubdateComparator;
import de.danoeh.antennapod.util.exception.MediaFileNotFoundException;
/**
@@ -40,6 +42,39 @@ public final class DBTasks {
}
/**
+ * Removes the feed with the given download url. This method should NOT be executed on the GUI thread.
+ * @param context Used for accessing the db
+ * @param downloadUrl URL of the feed.
+ * */
+ public static void removeFeedWithDownloadUrl(Context context, String downloadUrl) {
+ PodDBAdapter adapter = new PodDBAdapter(context);
+ adapter.open();
+ Cursor cursor = adapter.getFeedCursorDownloadUrls();
+ long feedID = 0;
+ if (cursor.moveToFirst()) {
+ do {
+ if (cursor.getString(1).equals(downloadUrl)) {
+ feedID = cursor.getLong(0);
+ }
+ } while (cursor.moveToNext());
+ }
+ cursor.close();
+ adapter.close();
+
+ if (feedID != 0) {
+ try {
+ DBWriter.deleteFeed(context, feedID).get();
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ } catch (ExecutionException e) {
+ e.printStackTrace();
+ }
+ } else {
+ Log.w(TAG, "removeFeedWithDownloadUrl: Could not find feed with url: " + downloadUrl);
+ }
+ }
+
+ /**
* Starts playback of a FeedMedia object's file. This method will build an Intent based on the given parameters to
* start the {@link PlaybackService}.
*
@@ -110,6 +145,8 @@ public final class DBTasks {
refreshFeeds(context, DBReader.getFeedList(context));
}
isRefreshing.set(false);
+
+ GpodnetSyncService.sendSyncIntent(context);
}
}.start();
} else {
@@ -406,12 +443,13 @@ public final class DBTasks {
private static int performAutoCleanup(final Context context,
final int episodeNumber) {
- List<FeedItem> candidates = DBReader.getDownloadedItems(context);
- List<FeedItem> queue = DBReader.getQueue(context);
+ List<FeedItem> candidates = new ArrayList<FeedItem>();
+ List<FeedItem> downloadedItems = DBReader.getDownloadedItems(context);
+ QueueAccess queue = QueueAccess.IDListAccess(DBReader.getQueueIDList(context));
List<FeedItem> delete;
- for (FeedItem item : candidates) {
+ for (FeedItem item : downloadedItems) {
if (item.hasMedia() && item.getMedia().isDownloaded()
- && !queue.contains(item) && item.isRead()) {
+ && !queue.contains(item.getId()) && item.isRead()) {
candidates.add(item);
}
@@ -440,7 +478,13 @@ public final class DBTasks {
}
for (FeedItem item : delete) {
- DBWriter.deleteFeedMediaOfItem(context, item.getId());
+ try {
+ DBWriter.deleteFeedMediaOfItem(context, item.getId()).get();
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ } catch (ExecutionException e) {
+ e.printStackTrace();
+ }
}
int counter = delete.size();
@@ -561,6 +605,7 @@ public final class DBTasks {
Log.d(TAG, "Feed with title " + newFeed.getTitle()
+ " already exists. Syncing new with existing one.");
+ Collections.sort(newFeed.getItems(), new FeedItemPubdateComparator());
savedFeed.setItems(DBReader.getFeedItemList(context, savedFeed));
if (savedFeed.compareWithOther(newFeed)) {
if (AppConfig.DEBUG)
@@ -578,7 +623,7 @@ public final class DBTasks {
final int i = idx;
item.setFeed(savedFeed);
savedFeed.getItems().add(i, item);
- DBWriter.markItemRead(context, item.getId(), false);
+ item.setRead(false);
} else {
oldItem.updateFromOther(item);
}
diff --git a/src/de/danoeh/antennapod/storage/DBWriter.java b/src/de/danoeh/antennapod/storage/DBWriter.java
index cef3a3cc2..f4ba8c237 100644
--- a/src/de/danoeh/antennapod/storage/DBWriter.java
+++ b/src/de/danoeh/antennapod/storage/DBWriter.java
@@ -4,6 +4,7 @@ import java.io.File;
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;
@@ -17,7 +18,9 @@ import android.preference.PreferenceManager;
import android.util.Log;
import de.danoeh.antennapod.AppConfig;
import de.danoeh.antennapod.feed.*;
+import de.danoeh.antennapod.preferences.GpodnetPreferences;
import de.danoeh.antennapod.preferences.PlaybackPreferences;
+import de.danoeh.antennapod.service.GpodnetSyncService;
import de.danoeh.antennapod.service.PlaybackService;
import de.danoeh.antennapod.service.download.DownloadStatus;
import de.danoeh.antennapod.util.QueueAccess;
@@ -101,6 +104,8 @@ public class DBWriter {
}
if (AppConfig.DEBUG)
Log.d(TAG, "Deleting File. Result: " + result);
+ EventDistributor.getInstance().sendQueueUpdateBroadcast();
+ EventDistributor.getInstance().sendUnreadItemsUpdateBroadcast();
}
}
});
@@ -171,6 +176,8 @@ public class DBWriter {
}
adapter.removeFeed(feed);
adapter.close();
+
+ GpodnetPreferences.addRemovedFeed(feed.getDownload_url());
EventDistributor.getInstance().sendFeedUpdateBroadcast();
}
}
@@ -215,7 +222,7 @@ public class DBWriter {
media.setPlaybackCompletionDate(new Date());
PodDBAdapter adapter = new PodDBAdapter(context);
adapter.open();
- adapter.setMedia(media);
+ adapter.setFeedMediaPlaybackCompletionDate(media);
adapter.close();
EventDistributor.getInstance().sendPlaybackHistoryUpdateBroadcast();
@@ -685,6 +692,7 @@ public class DBWriter {
adapter.setCompleteFeed(feed);
adapter.close();
+ GpodnetPreferences.addAddedFeed(feed.getDownload_url());
EventDistributor.getInstance().sendFeedUpdateBroadcast();
}
});
@@ -787,6 +795,26 @@ public class DBWriter {
});
}
+ /**
+ * Updates download URLs of feeds from a given Map. The key of the Map is the original URL of the feed
+ * and the value is the updated URL
+ * */
+ public static Future<?> updateFeedDownloadURLs(final Context context, final Map<String, String> urls) {
+ return dbExec.submit(new Runnable() {
+ @Override
+ public void run() {
+ PodDBAdapter adapter = new PodDBAdapter(context);
+ adapter.open();
+ for (String key : urls.keySet()) {
+ if (AppConfig.DEBUG) Log.d(TAG, "Replacing URL " + key + " with url " + urls.get(key));
+
+ adapter.setFeedDownloadUrl(key, urls.get(key));
+ }
+ adapter.close();
+ }
+ });
+ }
+
private static boolean itemListContains(List<FeedItem> items, long itemId) {
for (FeedItem item : items) {
if (item.getId() == itemId) {
diff --git a/src/de/danoeh/antennapod/storage/DownloadRequester.java b/src/de/danoeh/antennapod/storage/DownloadRequester.java
index 246b8bdfd..013162f0c 100644
--- a/src/de/danoeh/antennapod/storage/DownloadRequester.java
+++ b/src/de/danoeh/antennapod/storage/DownloadRequester.java
@@ -26,9 +26,9 @@ import de.danoeh.antennapod.util.URLChecker;
public class DownloadRequester {
private static final String TAG = "DownloadRequester";
- public static String IMAGE_DOWNLOADPATH = "images/";
- public static String FEED_DOWNLOADPATH = "cache/";
- public static String MEDIA_DOWNLOADPATH = "media/";
+ public static final String IMAGE_DOWNLOADPATH = "images/";
+ public static final String FEED_DOWNLOADPATH = "cache/";
+ public static final String MEDIA_DOWNLOADPATH = "media/";
private static DownloadRequester downloader;
@@ -38,7 +38,7 @@ public class DownloadRequester {
downloads = new ConcurrentHashMap<String, DownloadRequest>();
}
- public static DownloadRequester getInstance() {
+ public static synchronized DownloadRequester getInstance() {
if (downloader == null) {
downloader = new DownloadRequester();
}
diff --git a/src/de/danoeh/antennapod/storage/FeedItemStatistics.java b/src/de/danoeh/antennapod/storage/FeedItemStatistics.java
index 17e838761..6b79dd144 100644
--- a/src/de/danoeh/antennapod/storage/FeedItemStatistics.java
+++ b/src/de/danoeh/antennapod/storage/FeedItemStatistics.java
@@ -17,7 +17,7 @@ public class FeedItemStatistics {
this.numberOfItems = numberOfItems;
this.numberOfNewItems = numberOfNewItems;
this.numberOfInProgressItems = numberOfInProgressItems;
- this.lastUpdate = lastUpdate;
+ this.lastUpdate = (lastUpdate != null) ? (Date) lastUpdate.clone() : null;
}
public long getFeedID() {
@@ -37,6 +37,6 @@ public class FeedItemStatistics {
}
public Date getLastUpdate() {
- return lastUpdate;
+ return (lastUpdate != null) ? (Date) lastUpdate.clone() : null;
}
}
diff --git a/src/de/danoeh/antennapod/storage/PodDBAdapter.java b/src/de/danoeh/antennapod/storage/PodDBAdapter.java
index 78b4c6daa..6d41f6dfd 100644
--- a/src/de/danoeh/antennapod/storage/PodDBAdapter.java
+++ b/src/de/danoeh/antennapod/storage/PodDBAdapter.java
@@ -10,7 +10,6 @@ import android.database.DatabaseUtils;
import android.database.MergeCursor;
import android.database.SQLException;
import android.database.sqlite.SQLiteDatabase;
-import android.database.sqlite.SQLiteStatement;
import android.database.sqlite.SQLiteDatabase.CursorFactory;
import android.database.sqlite.SQLiteOpenHelper;
import android.util.Log;
@@ -235,7 +234,7 @@ public class PodDBAdapter {
/**
* Select id, description and content-encoded column from feeditems.
*/
- public static final String[] SEL_FI_EXTRA = {KEY_ID, KEY_DESCRIPTION,
+ private static final String[] SEL_FI_EXTRA = {KEY_ID, KEY_DESCRIPTION,
KEY_CONTENT_ENCODED, KEY_FEED};
// column indices for SEL_FI_EXTRA
@@ -279,6 +278,13 @@ public class PodDBAdapter {
//db.close();
}
+ public static boolean deleteDatabase(Context context) {
+ Log.w(TAG, "Deleting database");
+ dbHelperSingleton.close();
+ dbHelperSingleton = null;
+ return context.deleteDatabase(DATABASE_NAME);
+ }
+
/**
* Inserts or updates a feed entry
*
@@ -392,6 +398,17 @@ public class PodDBAdapter {
}
}
+ public void setFeedMediaPlaybackCompletionDate(FeedMedia media) {
+ if (media.getId() != 0) {
+ ContentValues values = new ContentValues();
+ values.put(KEY_PLAYBACK_COMPLETION_DATE, media.getPlaybackCompletionDate().getTime());
+ db.update(TABLE_NAME_FEED_MEDIA, values, KEY_ID + "=?",
+ new String[]{String.valueOf(media.getId())});
+ } else {
+ Log.e(TAG, "setFeedMediaPlaybackCompletionDate: ID of media was 0");
+ }
+ }
+
/**
* Insert all FeedItems of a feed and the feed object itself in a single
* transaction
@@ -408,6 +425,15 @@ public class PodDBAdapter {
db.endTransaction();
}
+ /**
+ * Updates the download URL of a Feed.
+ */
+ public void setFeedDownloadUrl(String original, String updated) {
+ ContentValues values = new ContentValues();
+ values.put(KEY_DOWNLOAD_URL, updated);
+ db.update(TABLE_NAME_FEEDS, values, KEY_DOWNLOAD_URL + "=?", new String[]{original});
+ }
+
public void setFeedItemlist(List<FeedItem> items) {
db.beginTransaction();
for (FeedItem item : items) {
@@ -642,6 +668,10 @@ public class PodDBAdapter {
return c;
}
+ public final Cursor getFeedCursorDownloadUrls() {
+ return db.query(TABLE_NAME_FEEDS, new String[]{KEY_ID, KEY_DOWNLOAD_URL}, null, null, null, null, null);
+ }
+
public final Cursor getExpiredFeedsCursor(long expirationTime) {
Cursor c = db.query(TABLE_NAME_FEEDS, null, "?<?", new String[]{
KEY_LASTUPDATE, String.valueOf(System.currentTimeMillis() - expirationTime)}, null, null,
@@ -764,7 +794,7 @@ public class PodDBAdapter {
final String query = "SELECT " + SEL_FI_SMALL_STR + " FROM " + TABLE_NAME_FEED_ITEMS
+ " INNER JOIN " + TABLE_NAME_FEED_MEDIA + " ON "
+ TABLE_NAME_FEED_ITEMS + "." + KEY_ID + "="
- + TABLE_NAME_FEED_MEDIA + "." + KEY_ID + " WHERE "
+ + TABLE_NAME_FEED_MEDIA + "." + KEY_FEEDITEM + " WHERE "
+ TABLE_NAME_FEED_MEDIA + "." + KEY_DOWNLOADED + ">0";
Cursor c = db.rawQuery(query, null);
return c;
@@ -993,7 +1023,7 @@ public class PodDBAdapter {
" MAX(pubDate) AS latest_episode," +
" COUNT(CASE WHEN position>0 THEN 1 END) AS in_progress," +
" COUNT(CASE WHEN downloaded=1 THEN 1 END) AS episodes_downloaded " +
- " FROM FeedItems INNER JOIN FeedMedia ON FeedItems.id=FeedMedia.feeditem GROUP BY FeedItems.feed)" +
+ " FROM FeedItems LEFT JOIN FeedMedia ON FeedItems.id=FeedMedia.feeditem GROUP BY FeedItems.feed)" +
" INNER JOIN Feeds ON Feeds.id = feed ORDER BY Feeds.title;";
public Cursor getFeedStatisticsCursor() {
diff --git a/src/de/danoeh/antennapod/syndication/namespace/atom/NSAtom.java b/src/de/danoeh/antennapod/syndication/namespace/atom/NSAtom.java
index 1efaac359..bcb0422ce 100644
--- a/src/de/danoeh/antennapod/syndication/namespace/atom/NSAtom.java
+++ b/src/de/danoeh/antennapod/syndication/namespace/atom/NSAtom.java
@@ -121,7 +121,7 @@ public class NSAtom extends Namespace {
if (state.getContentBuf() != null) {
content = state.getContentBuf().toString();
} else {
- content = new String();
+ content = "";
}
SyndElement topElement = state.getTagstack().peek();
String top = topElement.getName();
diff --git a/src/de/danoeh/antennapod/syndication/util/SyndDateUtils.java b/src/de/danoeh/antennapod/syndication/util/SyndDateUtils.java
index 30835434f..a1ed01354 100644
--- a/src/de/danoeh/antennapod/syndication/util/SyndDateUtils.java
+++ b/src/de/danoeh/antennapod/syndication/util/SyndDateUtils.java
@@ -11,7 +11,7 @@ import android.util.Log;
public class SyndDateUtils {
private static final String TAG = "DateUtils";
- public static final String[] RFC822DATES = { "dd MMM yy HH:mm:ss Z", };
+ private static final String[] RFC822DATES = { "dd MMM yy HH:mm:ss Z", };
/** RFC 3339 date format for UTC dates. */
public static final String RFC3339UTC = "yyyy-MM-dd'T'HH:mm:ss'Z'";
@@ -123,12 +123,12 @@ public class SyndDateUtils {
int idx = 0;
if (parts.length == 3) {
// string has hours
- result += Integer.valueOf(parts[idx]) * 3600000;
+ result += Integer.valueOf(parts[idx]) * 3600000L;
idx++;
}
- result += Integer.valueOf(parts[idx]) * 60000;
+ result += Integer.valueOf(parts[idx]) * 60000L;
idx++;
- result += (Float.valueOf(parts[idx])) * 1000;
+ result += (Float.valueOf(parts[idx])) * 1000L;
return result;
}
}
diff --git a/src/de/danoeh/antennapod/util/ChapterUtils.java b/src/de/danoeh/antennapod/util/ChapterUtils.java
index ac8149119..521bfebea 100644
--- a/src/de/danoeh/antennapod/util/ChapterUtils.java
+++ b/src/de/danoeh/antennapod/util/ChapterUtils.java
@@ -35,9 +35,9 @@ public class ChapterUtils {
* chapters.
*/
public static void readID3ChaptersFromPlayableStreamUrl(Playable p) {
- if (AppConfig.DEBUG)
- Log.d(TAG, "Reading id3 chapters from item " + p.getEpisodeTitle());
if (p != null && p.getStreamUrl() != null) {
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Reading id3 chapters from item " + p.getEpisodeTitle());
InputStream in = null;
try {
URL url = new URL(p.getStreamUrl());
@@ -86,9 +86,9 @@ public class ChapterUtils {
* chapters.
*/
public static void readID3ChaptersFromPlayableFileUrl(Playable p) {
- if (AppConfig.DEBUG)
- Log.d(TAG, "Reading id3 chapters from item " + p.getEpisodeTitle());
if (p != null && p.localFileAvailable() && p.getLocalMediaUrl() != null) {
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Reading id3 chapters from item " + p.getEpisodeTitle());
File source = new File(p.getLocalMediaUrl());
if (source.exists()) {
ChapterReader reader = new ChapterReader();
diff --git a/src/de/danoeh/antennapod/util/DuckType.java b/src/de/danoeh/antennapod/util/DuckType.java
new file mode 100644
index 000000000..0dfc01508
--- /dev/null
+++ b/src/de/danoeh/antennapod/util/DuckType.java
@@ -0,0 +1,115 @@
+/* Adapted from: http://thinking-in-code.blogspot.com/2008/11/duck-typing-in-java-using-dynamic.html */
+
+package de.danoeh.antennapod.util;
+
+import java.lang.reflect.InvocationHandler;
+import java.lang.reflect.Method;
+import java.lang.reflect.Proxy;
+
+/**
+ * Allows "duck typing" or dynamic invocation based on method signature rather
+ * than type hierarchy. In other words, rather than checking whether something
+ * IS-a duck, check whether it WALKS-like-a duck or QUACKS-like a duck.
+ *
+ * To use first use the coerce static method to indicate the object you want to
+ * do Duck Typing for, then specify an interface to the to method which you want
+ * to coerce the type to, e.g:
+ *
+ * public interface Foo { void aMethod(); } class Bar { ... public void
+ * aMethod() { ... } ... } Bar bar = ...; Foo foo =
+ * DuckType.coerce(bar).to(Foo.class); foo.aMethod();
+ *
+ *
+ */
+public class DuckType {
+
+ private final Object objectToCoerce;
+
+ private DuckType(Object objectToCoerce) {
+ this.objectToCoerce = objectToCoerce;
+ }
+
+ private class CoercedProxy implements InvocationHandler {
+ @Override
+ public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
+ Method delegateMethod = findMethodBySignature(method);
+ assert delegateMethod != null;
+ return delegateMethod.invoke(DuckType.this.objectToCoerce, args);
+ }
+ }
+
+ /**
+ * Specify the duck typed object to coerce.
+ *
+ * @param object
+ * the object to coerce
+ * @return
+ */
+ public static DuckType coerce(Object object) {
+ return new DuckType(object);
+ }
+
+ /**
+ * Coerce the Duck Typed object to the given interface providing it
+ * implements all the necessary methods.
+ *
+ * @param
+ * @param iface
+ * @return an instance of the given interface that wraps the duck typed
+ * class
+ * @throws ClassCastException
+ * if the object being coerced does not implement all the
+ * methods in the given interface.
+ */
+ @SuppressWarnings({ "rawtypes", "unchecked" })
+ public <T> T to(Class iface) {
+ assert iface.isInterface() : "cannot coerce object to a class, must be an interface";
+ if (isA(iface)) {
+ return (T) iface.cast(objectToCoerce);
+ }
+ if (quacksLikeA(iface)) {
+ return generateProxy(iface);
+ }
+ throw new ClassCastException("Could not coerce object of type " + objectToCoerce.getClass() + " to " + iface);
+ }
+
+ @SuppressWarnings("rawtypes")
+ private boolean isA(Class iface) {
+ return objectToCoerce.getClass().isInstance(iface);
+ }
+
+ /**
+ * Determine whether the duck typed object can be used with the given
+ * interface.
+ *
+ * @param Type
+ * of the interface to check.
+ * @param iface
+ * Interface class to check
+ * @return true if the object will support all the methods in the interface,
+ * false otherwise.
+ */
+ @SuppressWarnings("rawtypes")
+ public boolean quacksLikeA(Class iface) {
+ for (Method method : iface.getMethods()) {
+ if (findMethodBySignature(method) == null) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ @SuppressWarnings({ "unchecked", "rawtypes" })
+ private <T> T generateProxy(Class iface) {
+ return (T) Proxy.newProxyInstance(iface.getClassLoader(), new Class[] { iface }, new CoercedProxy());
+ }
+
+ private Method findMethodBySignature(Method method) {
+ try {
+ return objectToCoerce.getClass().getMethod(method.getName(), method.getParameterTypes());
+ } catch (NoSuchMethodException e) {
+ return null;
+ }
+ }
+
+} \ No newline at end of file
diff --git a/src/de/danoeh/antennapod/util/LangUtils.java b/src/de/danoeh/antennapod/util/LangUtils.java
index 53f8de773..e6e1d8399 100644
--- a/src/de/danoeh/antennapod/util/LangUtils.java
+++ b/src/de/danoeh/antennapod/util/LangUtils.java
@@ -1,8 +1,11 @@
package de.danoeh.antennapod.util;
+import java.nio.charset.Charset;
import java.util.HashMap;
public class LangUtils {
+ public static final Charset UTF_8 = Charset.forName("UTF-8");
+
private static HashMap<String, String> languages;
static {
languages = new HashMap<String, String>();
diff --git a/src/de/danoeh/antennapod/util/NetworkUtils.java b/src/de/danoeh/antennapod/util/NetworkUtils.java
index de7b854cc..278f7ad7a 100644
--- a/src/de/danoeh/antennapod/util/NetworkUtils.java
+++ b/src/de/danoeh/antennapod/util/NetworkUtils.java
@@ -60,4 +60,10 @@ public class NetworkUtils {
Log.d(TAG, "Network for auto-dl is not available");
return false;
}
+
+ public static boolean networkAvailable(Context context) {
+ ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
+ NetworkInfo info = cm.getActiveNetworkInfo();
+ return info != null && info.isConnected();
+ }
}
diff --git a/src/de/danoeh/antennapod/util/URLChecker.java b/src/de/danoeh/antennapod/util/URLChecker.java
index 6d9b8ff03..13668d4a9 100644
--- a/src/de/danoeh/antennapod/util/URLChecker.java
+++ b/src/de/danoeh/antennapod/util/URLChecker.java
@@ -19,13 +19,12 @@ public final class URLChecker {
* */
public static String prepareURL(String url) {
StringBuilder builder = new StringBuilder();
- url = url.trim();
- if (!url.startsWith("http")) {
+ if (url.startsWith("feed://")) {
+ if (AppConfig.DEBUG) Log.d(TAG, "Replacing feed:// with http://");
+ url = url.replace("feed://", "http://");
+ } else if (!(url.startsWith("http://") || url.startsWith("https://"))) {
+ if (AppConfig.DEBUG) Log.d(TAG, "Adding http:// at the beginning of the URL");
builder.append("http://");
- if (AppConfig.DEBUG) Log.d(TAG, "Missing http; appending");
- } else if (url.startsWith("https")) {
- if (AppConfig.DEBUG) Log.d(TAG, "Replacing https with http");
- url = url.replaceFirst("https", "http");
}
builder.append(url);
diff --git a/src/de/danoeh/antennapod/util/comparator/DownloadStatusComparator.java b/src/de/danoeh/antennapod/util/comparator/DownloadStatusComparator.java
index 2cfe52364..d0561252f 100644
--- a/src/de/danoeh/antennapod/util/comparator/DownloadStatusComparator.java
+++ b/src/de/danoeh/antennapod/util/comparator/DownloadStatusComparator.java
@@ -9,8 +9,7 @@ public class DownloadStatusComparator implements Comparator<DownloadStatus> {
@Override
public int compare(DownloadStatus lhs, DownloadStatus rhs) {
- return -lhs.getCompletionDate().compareTo(rhs.getCompletionDate());
-
+ return rhs.getCompletionDate().compareTo(lhs.getCompletionDate());
}
}
diff --git a/src/de/danoeh/antennapod/util/comparator/FeedItemPubdateComparator.java b/src/de/danoeh/antennapod/util/comparator/FeedItemPubdateComparator.java
index b9ee6c07e..c95c0833c 100644
--- a/src/de/danoeh/antennapod/util/comparator/FeedItemPubdateComparator.java
+++ b/src/de/danoeh/antennapod/util/comparator/FeedItemPubdateComparator.java
@@ -13,7 +13,7 @@ public class FeedItemPubdateComparator implements Comparator<FeedItem> {
}*/
@Override
public int compare(FeedItem lhs, FeedItem rhs) {
- return -lhs.getPubDate().compareTo(rhs.getPubDate());
+ return rhs.getPubDate().compareTo(lhs.getPubDate());
}
}
diff --git a/src/de/danoeh/antennapod/util/comparator/PlaybackCompletionDateComparator.java b/src/de/danoeh/antennapod/util/comparator/PlaybackCompletionDateComparator.java
index 2d0ce75ca..434a5a956 100644
--- a/src/de/danoeh/antennapod/util/comparator/PlaybackCompletionDateComparator.java
+++ b/src/de/danoeh/antennapod/util/comparator/PlaybackCompletionDateComparator.java
@@ -11,8 +11,8 @@ public class PlaybackCompletionDateComparator implements Comparator<FeedItem> {
&& lhs.getMedia().getPlaybackCompletionDate() != null
&& rhs.getMedia() != null
&& rhs.getMedia().getPlaybackCompletionDate() != null) {
- return -lhs.getMedia().getPlaybackCompletionDate()
- .compareTo(rhs.getMedia().getPlaybackCompletionDate());
+ return rhs.getMedia().getPlaybackCompletionDate()
+ .compareTo(lhs.getMedia().getPlaybackCompletionDate());
}
return 0;
}
diff --git a/src/de/danoeh/antennapod/util/menuhandler/FeedItemMenuHandler.java b/src/de/danoeh/antennapod/util/menuhandler/FeedItemMenuHandler.java
index 0116dbf21..aad240fc7 100644
--- a/src/de/danoeh/antennapod/util/menuhandler/FeedItemMenuHandler.java
+++ b/src/de/danoeh/antennapod/util/menuhandler/FeedItemMenuHandler.java
@@ -51,10 +51,13 @@ public class FeedItemMenuHandler {
* 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 Always returns true
+ * @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();
@@ -129,7 +132,7 @@ public class FeedItemMenuHandler {
false);
break;
case R.id.remove_item:
- DBWriter.deleteFeedMediaOfItem(context, selectedItem.getId());
+ DBWriter.deleteFeedMediaOfItem(context, selectedItem.getMedia().getId());
break;
case R.id.cancel_download_item:
requester.cancelDownload(context, selectedItem.getMedia());
diff --git a/src/de/danoeh/antennapod/util/menuhandler/FeedMenuHandler.java b/src/de/danoeh/antennapod/util/menuhandler/FeedMenuHandler.java
index 843607617..446e024d9 100644
--- a/src/de/danoeh/antennapod/util/menuhandler/FeedMenuHandler.java
+++ b/src/de/danoeh/antennapod/util/menuhandler/FeedMenuHandler.java
@@ -30,6 +30,10 @@ public class FeedMenuHandler {
}
public static boolean onPrepareOptionsMenu(Menu menu, Feed selectedFeed) {
+ if (selectedFeed == null) {
+ return false;
+ }
+
if (AppConfig.DEBUG)
Log.d(TAG, "Preparing options menu");
menu.findItem(R.id.mark_all_read_item).setVisible(
diff --git a/src/de/danoeh/antennapod/util/playback/AudioPlayer.java b/src/de/danoeh/antennapod/util/playback/AudioPlayer.java
new file mode 100644
index 000000000..68d31324d
--- /dev/null
+++ b/src/de/danoeh/antennapod/util/playback/AudioPlayer.java
@@ -0,0 +1,30 @@
+package de.danoeh.antennapod.util.playback;
+
+import android.content.Context;
+import android.util.Log;
+import android.view.SurfaceHolder;
+
+import com.aocate.media.MediaPlayer;
+
+public class AudioPlayer extends MediaPlayer implements IPlayer {
+ private static final String TAG = "AudioPlayer";
+
+ public AudioPlayer(Context context) {
+ super(context);
+ }
+
+ @Override
+ public void setScreenOnWhilePlaying(boolean screenOn) {
+ Log.e(TAG, "Setting screen on while playing not supported in Audio Player");
+ throw new UnsupportedOperationException("Setting screen on while playing not supported in Audio Player");
+
+ }
+
+ @Override
+ public void setDisplay(SurfaceHolder sh) {
+ if (sh != null) {
+ Log.e(TAG, "Setting display not supported in Audio Player");
+ throw new UnsupportedOperationException("Setting display not supported in Audio Player");
+ }
+ }
+}
diff --git a/src/de/danoeh/antennapod/util/playback/ExternalMedia.java b/src/de/danoeh/antennapod/util/playback/ExternalMedia.java
index 1ada0ec03..e937ee437 100644
--- a/src/de/danoeh/antennapod/util/playback/ExternalMedia.java
+++ b/src/de/danoeh/antennapod/util/playback/ExternalMedia.java
@@ -25,7 +25,6 @@ public class ExternalMedia implements Playable {
private String episodeTitle;
private String feedTitle;
- private String shownotes;
private MediaType mediaType = MediaType.AUDIO;
private List<Chapter> chapters;
private int duration;
@@ -80,8 +79,13 @@ public class ExternalMedia implements Playable {
.extractMetadata(MediaMetadataRetriever.METADATA_KEY_TITLE);
feedTitle = mmr
.extractMetadata(MediaMetadataRetriever.METADATA_KEY_ALBUM);
- duration = Integer.parseInt(mmr
+ try {
+ duration = Integer.parseInt(mmr
.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION));
+ } catch (NumberFormatException e) {
+ e.printStackTrace();
+ throw new PlayableException("NumberFormatException when reading duration of media file");
+ }
ChapterUtils.loadChaptersFromFileUrl(this);
}
diff --git a/src/de/danoeh/antennapod/util/playback/IPlayer.java b/src/de/danoeh/antennapod/util/playback/IPlayer.java
new file mode 100644
index 000000000..ca9b36358
--- /dev/null
+++ b/src/de/danoeh/antennapod/util/playback/IPlayer.java
@@ -0,0 +1,64 @@
+package de.danoeh.antennapod.util.playback;
+
+import java.io.IOException;
+
+import android.view.SurfaceHolder;
+
+public interface IPlayer {
+ boolean canSetPitch();
+
+ boolean canSetSpeed();
+
+ float getCurrentPitchStepsAdjustment();
+
+ int getCurrentPosition();
+
+ float getCurrentSpeedMultiplier();
+
+ int getDuration();
+
+ float getMaxSpeedMultiplier();
+
+ float getMinSpeedMultiplier();
+
+ boolean isLooping();
+
+ boolean isPlaying();
+
+ void pause();
+
+ void prepare() throws IllegalStateException, IOException;
+
+ void prepareAsync();
+
+ void release();
+
+ void reset();
+
+ void seekTo(int msec);
+
+ void setAudioStreamType(int streamtype);
+
+ void setScreenOnWhilePlaying(boolean screenOn);
+
+ void setDataSource(String path) throws IllegalStateException, IOException,
+ IllegalArgumentException, SecurityException;
+
+ void setDisplay(SurfaceHolder sh);
+
+ void setEnableSpeedAdjustment(boolean enableSpeedAdjustment);
+
+ void setLooping(boolean looping);
+
+ void setPitchStepsAdjustment(float pitchSteps);
+
+ void setPlaybackPitch(float f);
+
+ void setPlaybackSpeed(float f);
+
+ void setVolume(float left, float right);
+
+ void start();
+
+ void stop();
+}
diff --git a/src/de/danoeh/antennapod/util/playback/PlaybackController.java b/src/de/danoeh/antennapod/util/playback/PlaybackController.java
index 5a5b43a6e..f5d1847b3 100644
--- a/src/de/danoeh/antennapod/util/playback/PlaybackController.java
+++ b/src/de/danoeh/antennapod/util/playback/PlaybackController.java
@@ -342,6 +342,9 @@ public abstract class PlaybackController {
case PlaybackService.NOTIFICATION_TYPE_PLAYBACK_END:
onPlaybackEnd();
break;
+ case PlaybackService.NOTIFICATION_TYPE_PLAYBACK_SPEED_CHANGE:
+ onPlaybackSpeedChange();
+ break;
}
} else {
@@ -369,6 +372,8 @@ public abstract class PlaybackController {
}
};
+ public abstract void onPlaybackSpeedChange();
+
public abstract void onShutdownNotification();
/**
@@ -663,6 +668,24 @@ public abstract class PlaybackController {
return status;
}
+ public boolean canSetPlaybackSpeed() {
+ 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 boolean isPlayingVideo() {
if (playbackService != null) {
return PlaybackService.isPlayingVideo();
@@ -670,6 +693,7 @@ public abstract class PlaybackController {
return false;
}
+
/**
* Returns true if PlaybackController can communicate with the playback
* service.
diff --git a/src/de/danoeh/antennapod/util/playback/VideoPlayer.java b/src/de/danoeh/antennapod/util/playback/VideoPlayer.java
new file mode 100644
index 000000000..f0a50542c
--- /dev/null
+++ b/src/de/danoeh/antennapod/util/playback/VideoPlayer.java
@@ -0,0 +1,62 @@
+package de.danoeh.antennapod.util.playback;
+
+import android.media.MediaPlayer;
+import android.util.Log;
+
+public class VideoPlayer extends MediaPlayer implements IPlayer {
+ private static final String TAG = "VideoPlayer";
+
+ @Override
+ public boolean canSetPitch() {
+ return false;
+ }
+
+ @Override
+ public boolean canSetSpeed() {
+ return false;
+ }
+
+ @Override
+ public float getCurrentPitchStepsAdjustment() {
+ return 1;
+ }
+
+ @Override
+ public float getCurrentSpeedMultiplier() {
+ return 1;
+ }
+
+ @Override
+ public float getMaxSpeedMultiplier() {
+ return 1;
+ }
+
+ @Override
+ public float getMinSpeedMultiplier() {
+ return 1;
+ }
+
+ @Override
+ public void setEnableSpeedAdjustment(boolean enableSpeedAdjustment) throws UnsupportedOperationException {
+ Log.e(TAG, "Setting enable speed adjustment unsupported in video player");
+ throw new UnsupportedOperationException("Setting enable speed adjustment unsupported in video player");
+ }
+
+ @Override
+ public void setPitchStepsAdjustment(float pitchSteps) {
+ Log.e(TAG, "Setting pitch steps adjustment unsupported in video player");
+ throw new UnsupportedOperationException("Setting pitch steps adjustment unsupported in video player");
+ }
+
+ @Override
+ public void setPlaybackPitch(float f) {
+ Log.e(TAG, "Setting playback pitch unsupported in video player");
+ throw new UnsupportedOperationException("Setting playback pitch unsupported in video player");
+ }
+
+ @Override
+ public void setPlaybackSpeed(float f) {
+ Log.e(TAG, "Setting playback speed unsupported in video player");
+ throw new UnsupportedOperationException("Setting playback speed unsupported in video player");
+ }
+}