diff options
author | Tom Hennen <tom.hennen@gmail.com> | 2013-09-09 18:21:00 -0400 |
---|---|---|
committer | Tom Hennen <tom.hennen@gmail.com> | 2013-09-09 18:21:00 -0400 |
commit | 34a5e62339928d9a6353ba966cf9841e1719b570 (patch) | |
tree | e465a9c389323604c48d854603bd21ef89dd800a /src/de/danoeh/antennapod | |
parent | 4e845b83c650b0f103b564da221326b997e8d032 (diff) | |
parent | 02926a6e5ffa968d08efeae5012a0ecf41a6f33a (diff) | |
download | AntennaPod-34a5e62339928d9a6353ba966cf9841e1719b570.zip |
Merge branch 'develop' of https://github.com/danieloeh/AntennaPod into move-to-top
Diffstat (limited to 'src/de/danoeh/antennapod')
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"); + } +} |