diff options
Diffstat (limited to 'src/de/danoeh')
79 files changed, 2260 insertions, 1377 deletions
diff --git a/src/de/danoeh/antennapod/AppConfig.java b/src/de/danoeh/antennapod/AppConfig.java index cac946f84..0e12a350f 100644 --- a/src/de/danoeh/antennapod/AppConfig.java +++ b/src/de/danoeh/antennapod/AppConfig.java @@ -2,6 +2,6 @@ package de.danoeh.antennapod; public final class AppConfig { /** Should be used when setting User-Agent header for HTTP-requests. */ - public final static String USER_AGENT = "AntennaPod/0.9.9.1"; + public final static String USER_AGENT = "AntennaPod/0.9.9.2"; } diff --git a/src/de/danoeh/antennapod/activity/AudioplayerActivity.java b/src/de/danoeh/antennapod/activity/AudioplayerActivity.java index 090c3f1f5..6373ff240 100644 --- a/src/de/danoeh/antennapod/activity/AudioplayerActivity.java +++ b/src/de/danoeh/antennapod/activity/AudioplayerActivity.java @@ -12,32 +12,48 @@ import android.support.v4.app.FragmentTransaction; import android.support.v4.app.ListFragment; import android.support.v4.widget.DrawerLayout; import android.util.Log; +import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.View.OnClickListener; import android.view.View.OnLongClickListener; -import android.view.Window; -import android.widget.*; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; +import android.widget.Button; +import android.widget.ImageButton; import android.widget.ImageView.ScaleType; +import android.widget.ListView; +import android.widget.TextView; + +import org.apache.commons.lang3.StringUtils; + import de.danoeh.antennapod.BuildConfig; import de.danoeh.antennapod.R; import de.danoeh.antennapod.adapter.ChapterListAdapter; import de.danoeh.antennapod.adapter.NavListAdapter; import de.danoeh.antennapod.asynctask.ImageLoader; import de.danoeh.antennapod.dialog.VariableSpeedDialog; -import de.danoeh.antennapod.feed.*; +import de.danoeh.antennapod.feed.Chapter; +import de.danoeh.antennapod.feed.EventDistributor; +import de.danoeh.antennapod.feed.Feed; +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.service.playback.PlaybackService; import de.danoeh.antennapod.storage.DBReader; +import de.danoeh.antennapod.util.menuhandler.MenuItemUtils; +import de.danoeh.antennapod.util.menuhandler.NavDrawerActivity; import de.danoeh.antennapod.util.playback.ExternalMedia; import de.danoeh.antennapod.util.playback.Playable; +import de.danoeh.antennapod.util.playback.PlaybackController; /** * Activity for playing audio files. */ -public class AudioplayerActivity extends MediaplayerActivity { +public class AudioplayerActivity extends MediaplayerActivity implements ItemDescriptionFragment.ItemDescriptionFragmentCallback, + NavDrawerActivity { private static final int POS_COVER = 0; private static final int POS_DESCR = 1; private static final int POS_CHAPTERS = 2; @@ -123,7 +139,6 @@ public class AudioplayerActivity extends MediaplayerActivity { @Override protected void onCreate(Bundle savedInstanceState) { - requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS); super.onCreate(savedInstanceState); getSupportActionBar().setDisplayShowTitleEnabled(false); detachedFragments = new Fragment[NUM_CONTENT_FRAGMENTS]; @@ -215,8 +230,7 @@ public class AudioplayerActivity extends MediaplayerActivity { @Override protected void onResume() { super.onResume(); - if (getIntent().getAction() != null - && getIntent().getAction().equals(Intent.ACTION_VIEW)) { + if (StringUtils.equals(getIntent().getAction(), Intent.ACTION_VIEW)) { Intent intent = getIntent(); if (BuildConfig.DEBUG) Log.d(TAG, "Received VIEW intent: " @@ -293,7 +307,7 @@ public class AudioplayerActivity extends MediaplayerActivity { case POS_DESCR: if (descriptionFragment == null) { descriptionFragment = ItemDescriptionFragment - .newInstance(media, true); + .newInstance(media, true, true); } currentlyShownFragment = descriptionFragment; break; @@ -427,6 +441,7 @@ public class AudioplayerActivity extends MediaplayerActivity { }; typedArray.recycle(); drawerToggle.setDrawerIndicatorEnabled(false); + drawerLayout.setDrawerListener(drawerToggle); navAdapter = new NavListAdapter(itemAccess, this); navList.setAdapter(navAdapter); @@ -603,6 +618,34 @@ public class AudioplayerActivity extends MediaplayerActivity { clearStatusMsg(); } + @Override + public PlaybackController getPlaybackController() { + return controller; + } + + @Override + public boolean isDrawerOpen() { + return drawerLayout != null && navList != null && drawerLayout.isDrawerOpen(navList); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + if (!MenuItemUtils.isActivityDrawerOpen(this)) { + return super.onCreateOptionsMenu(menu); + } else { + return false; + } + } + + @Override + public boolean onPrepareOptionsMenu(Menu menu) { + if (!MenuItemUtils.isActivityDrawerOpen(this)) { + return super.onPrepareOptionsMenu(menu); + } else { + return false; + } + } + public interface AudioplayerContentFragment { public void onDataSetChanged(Playable media); } @@ -615,7 +658,7 @@ public class AudioplayerActivity extends MediaplayerActivity { @Override public boolean onOptionsItemSelected(MenuItem item) { - if (drawerToggle.onOptionsItemSelected(item)) { + if (drawerToggle != null && drawerToggle.onOptionsItemSelected(item)) { return true; } else { return super.onOptionsItemSelected(item); diff --git a/src/de/danoeh/antennapod/activity/DefaultOnlineFeedViewActivity.java b/src/de/danoeh/antennapod/activity/DefaultOnlineFeedViewActivity.java index 597189885..e89f8d05c 100644 --- a/src/de/danoeh/antennapod/activity/DefaultOnlineFeedViewActivity.java +++ b/src/de/danoeh/antennapod/activity/DefaultOnlineFeedViewActivity.java @@ -5,19 +5,26 @@ import android.content.Intent; import android.os.AsyncTask; import android.os.Bundle; import android.support.v4.app.NavUtils; +import android.util.Log; import android.view.LayoutInflater; import android.view.MenuItem; import android.view.View; import android.widget.*; +import de.danoeh.antennapod.BuildConfig; 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.feed.FeedItem; import de.danoeh.antennapod.storage.DBReader; import de.danoeh.antennapod.storage.DownloadRequestException; import de.danoeh.antennapod.storage.DownloadRequester; +import org.apache.commons.lang3.StringUtils; +import org.jsoup.Jsoup; +import org.jsoup.examples.HtmlToPlainText; +import org.jsoup.nodes.Document; import java.util.ArrayList; import java.util.Date; @@ -25,9 +32,11 @@ import java.util.List; import java.util.Map; /** - * Created by daniel on 24.08.13. + * Default implementation of OnlineFeedViewActivity. Shows the downloaded feed's items with their descriptions, + * a subscribe button and a spinner for choosing alternate feed URLs. */ public class DefaultOnlineFeedViewActivity extends OnlineFeedViewActivity { + private static final String TAG = "DefaultOnlineFeedViewActivity"; private static final int EVENTS = EventDistributor.DOWNLOAD_HANDLED | EventDistributor.DOWNLOAD_QUEUED | EventDistributor.FEED_LIST_UPDATE; private volatile List<Feed> feeds; @@ -64,6 +73,24 @@ public class DefaultOnlineFeedViewActivity extends OnlineFeedViewActivity { } @Override + protected void beforeShowFeedInformation(Feed feed, Map<String, String> alternateFeedUrls) { + super.beforeShowFeedInformation(feed, alternateFeedUrls); + + // remove HTML tags from descriptions + + if (BuildConfig.DEBUG) Log.d(TAG, "Removing HTML from shownotes"); + if (feed.getItems() != null) { + HtmlToPlainText formatter = new HtmlToPlainText(); + for (FeedItem item : feed.getItems()) { + if (item.getDescription() != null) { + Document description = Jsoup.parse(item.getDescription()); + item.setDescription(StringUtils.trim(formatter.getPlainText(description))); + } + } + } + } + + @Override protected void showFeedInformation(final Feed feed, final Map<String, String> alternateFeedUrls) { super.showFeedInformation(feed, alternateFeedUrls); setContentView(R.layout.listview_activity); @@ -74,7 +101,7 @@ public class DefaultOnlineFeedViewActivity extends OnlineFeedViewActivity { ListView listView = (ListView) findViewById(R.id.listview); LayoutInflater inflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE); - View header = inflater.inflate(R.layout.onlinefeedview_header, null); + View header = inflater.inflate(R.layout.onlinefeedview_header, listView, false); listView.addHeaderView(header); listView.setAdapter(new FeedItemlistDescriptionAdapter(this, 0, feed.getItems())); @@ -131,7 +158,7 @@ public class DefaultOnlineFeedViewActivity extends OnlineFeedViewActivity { for (String url : alternateFeedUrls.keySet()) { alternateUrlsTitleList.add(alternateFeedUrls.get(url)); } - ArrayAdapter<String> adapter = new ArrayAdapter<String>(this, android.R.layout.simple_spinner_item, alternateUrlsTitleList); + ArrayAdapter<String> adapter = new ArrayAdapter<String>(this, android.R.layout.simple_spinner_item, alternateUrlsTitleList); adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); spAlternateUrls.setAdapter(adapter); spAlternateUrls.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { diff --git a/src/de/danoeh/antennapod/activity/DownloadAuthenticationActivity.java b/src/de/danoeh/antennapod/activity/DownloadAuthenticationActivity.java index 4b8420e45..c5f25d813 100644 --- a/src/de/danoeh/antennapod/activity/DownloadAuthenticationActivity.java +++ b/src/de/danoeh/antennapod/activity/DownloadAuthenticationActivity.java @@ -9,6 +9,9 @@ import android.view.View; import android.widget.Button; import android.widget.EditText; import android.widget.TextView; + +import org.apache.commons.lang3.Validate; + import de.danoeh.antennapod.BuildConfig; import de.danoeh.antennapod.R; import de.danoeh.antennapod.preferences.UserPreferences; @@ -58,7 +61,8 @@ public class DownloadAuthenticationActivity extends ActionBarActivity { butCancel = (Button) findViewById(R.id.butCancel); txtvDescription = (TextView) findViewById(R.id.txtvDescription); - if (!getIntent().hasExtra(ARG_DOWNLOAD_REQUEST)) throw new IllegalArgumentException("Download request missing"); + Validate.isTrue(getIntent().hasExtra(ARG_DOWNLOAD_REQUEST), "Download request missing"); + request = getIntent().getParcelableExtra(ARG_DOWNLOAD_REQUEST); sendToDownloadRequester = getIntent().getBooleanExtra(ARG_SEND_TO_DOWNLOAD_REQUESTER_BOOL, false); diff --git a/src/de/danoeh/antennapod/activity/MainActivity.java b/src/de/danoeh/antennapod/activity/MainActivity.java index 257bea82d..b7014dab2 100644 --- a/src/de/danoeh/antennapod/activity/MainActivity.java +++ b/src/de/danoeh/antennapod/activity/MainActivity.java @@ -19,6 +19,9 @@ import android.util.Log; import android.view.*; import android.widget.AdapterView; import android.widget.ListView; + +import org.apache.commons.lang3.Validate; + import de.danoeh.antennapod.BuildConfig; import de.danoeh.antennapod.R; import de.danoeh.antennapod.adapter.NavListAdapter; @@ -28,13 +31,14 @@ import de.danoeh.antennapod.fragment.*; import de.danoeh.antennapod.preferences.UserPreferences; import de.danoeh.antennapod.storage.DBReader; import de.danoeh.antennapod.util.StorageUtils; +import de.danoeh.antennapod.util.menuhandler.NavDrawerActivity; import java.util.List; /** * The activity that is shown when the user launches the app. */ -public class MainActivity extends ActionBarActivity { +public class MainActivity extends ActionBarActivity implements NavDrawerActivity{ private static final String TAG = "MainActivity"; private static final int EVENTS = EventDistributor.DOWNLOAD_HANDLED | EventDistributor.DOWNLOAD_QUEUED @@ -61,7 +65,7 @@ public class MainActivity extends ActionBarActivity { private ListView navList; private NavListAdapter navAdapter; - private ActionBarDrawerToggle drawerToogle; + private ActionBarDrawerToggle drawerToggle; private CharSequence drawerTitle; private CharSequence currentTitle; @@ -71,7 +75,7 @@ public class MainActivity extends ActionBarActivity { public void onCreate(Bundle savedInstanceState) { setTheme(UserPreferences.getTheme()); super.onCreate(savedInstanceState); - requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS); + supportRequestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS); StorageUtils.checkStorageAvailability(this); setContentView(R.layout.main); setVolumeControlStream(AudioManager.STREAM_MUSIC); @@ -82,7 +86,7 @@ public class MainActivity extends ActionBarActivity { navList = (ListView) findViewById(R.id.nav_list); TypedArray typedArray = obtainStyledAttributes(new int[]{R.attr.nav_drawer_toggle}); - drawerToogle = new ActionBarDrawerToggle(this, drawerLayout, typedArray.getResourceId(0, 0), R.string.drawer_open, R.string.drawer_close) { + drawerToggle = new ActionBarDrawerToggle(this, drawerLayout, typedArray.getResourceId(0, 0), R.string.drawer_open, R.string.drawer_close) { @Override public void onDrawerOpened(View drawerView) { super.onDrawerOpened(drawerView); @@ -101,7 +105,7 @@ public class MainActivity extends ActionBarActivity { }; typedArray.recycle(); - drawerLayout.setDrawerListener(drawerToogle); + drawerLayout.setDrawerListener(drawerToggle); FragmentManager fm = getSupportFragmentManager(); FragmentTransaction transaction = fm.beginTransaction(); @@ -147,6 +151,10 @@ public class MainActivity extends ActionBarActivity { return getSupportActionBar(); } + public boolean isDrawerOpen() { + return drawerLayout != null && navList != null && drawerLayout.isDrawerOpen(navList); + } + public List<Feed> getFeeds() { return (navDrawerData != null) ? navDrawerData.feeds : null; } @@ -219,7 +227,7 @@ public class MainActivity extends ActionBarActivity { } public void loadChildFragment(Fragment fragment) { - if (fragment == null) throw new IllegalArgumentException("fragment = null"); + Validate.notNull(fragment); FragmentManager fm = getSupportFragmentManager(); fm.beginTransaction() .replace(R.id.main_view, fragment, "main") @@ -244,7 +252,7 @@ public class MainActivity extends ActionBarActivity { @Override protected void onPostCreate(Bundle savedInstanceState) { super.onPostCreate(savedInstanceState); - drawerToogle.syncState(); + drawerToggle.syncState(); if (savedInstanceState != null) { currentTitle = savedInstanceState.getString("title"); if (!drawerLayout.isDrawerOpen(navList)) { @@ -257,7 +265,7 @@ public class MainActivity extends ActionBarActivity { @Override public void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); - drawerToogle.onConfigurationChanged(newConfig); + drawerToggle.onConfigurationChanged(newConfig); } @Override @@ -296,7 +304,7 @@ public class MainActivity extends ActionBarActivity { @Override public boolean onOptionsItemSelected(MenuItem item) { - if (drawerToogle.onOptionsItemSelected(item)) { + if (drawerToggle.onOptionsItemSelected(item)) { return true; } switch (item.getItemId()) { @@ -311,7 +319,6 @@ public class MainActivity extends ActionBarActivity { @Override public boolean onPrepareOptionsMenu(Menu menu) { super.onPrepareOptionsMenu(menu); - return true; } diff --git a/src/de/danoeh/antennapod/activity/MediaplayerActivity.java b/src/de/danoeh/antennapod/activity/MediaplayerActivity.java index fc70f4c05..13e7b8a82 100644 --- a/src/de/danoeh/antennapod/activity/MediaplayerActivity.java +++ b/src/de/danoeh/antennapod/activity/MediaplayerActivity.java @@ -3,19 +3,25 @@ package de.danoeh.antennapod.activity; import android.app.AlertDialog; import android.content.DialogInterface; import android.content.Intent; +import android.content.res.TypedArray; import android.graphics.PixelFormat; import android.media.AudioManager; import android.net.Uri; +import android.os.Build; import android.os.Bundle; +import android.support.v4.app.DialogFragment; import android.support.v7.app.ActionBarActivity; import android.util.Log; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; +import android.view.Window; import android.widget.ImageButton; import android.widget.SeekBar; import android.widget.SeekBar.OnSeekBarChangeListener; import android.widget.TextView; +import com.doomonafireball.betterpickers.hmspicker.HmsPickerBuilder; +import com.doomonafireball.betterpickers.hmspicker.HmsPickerDialogFragment; import de.danoeh.antennapod.BuildConfig; import de.danoeh.antennapod.R; import de.danoeh.antennapod.dialog.TimeDialog; @@ -30,6 +36,7 @@ import de.danoeh.antennapod.util.StorageUtils; import de.danoeh.antennapod.util.playback.MediaPlayerError; import de.danoeh.antennapod.util.playback.Playable; import de.danoeh.antennapod.util.playback.PlaybackController; +import org.shredzone.flattr4j.model.User; /** * Provides general features which are both needed for playing audio and video @@ -164,6 +171,10 @@ public abstract class MediaplayerActivity extends ActionBarActivity protected void onCreate(Bundle savedInstanceState) { chooseTheme(); super.onCreate(savedInstanceState); + + // subclasses might use this feature + supportRequestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS); + if (BuildConfig.DEBUG) Log.d(TAG, "Creating Activity"); StorageUtils.checkStorageAvailability(this); @@ -312,16 +323,34 @@ public abstract class MediaplayerActivity extends ActionBarActivity break; case R.id.set_sleeptimer_item: if (controller.serviceAvailable()) { - TimeDialog td = new TimeDialog(this, - R.string.set_sleeptimer_label, - R.string.set_sleeptimer_label) { - - @Override - public void onTimeEntered(long millis) { - controller.setSleepTimer(millis); - } - }; - td.show(); + int pickerStyle = (UserPreferences.getTheme() == R.style.Theme_AntennaPod_Light) ? + R.style.AntennaPodBetterPickerThemeLight : R.style.AntennaPodBetterPickerThemeDark; + if (Build.VERSION.SDK_INT > 10) { // TODO remove this as soon as dialog is shown correctly on 2.3 + HmsPickerBuilder hpb = new HmsPickerBuilder() + .setStyleResId(pickerStyle) + .setFragmentManager(getSupportFragmentManager()); + + hpb.addHmsPickerDialogHandler(new HmsPickerDialogFragment.HmsPickerDialogHandler() { + @Override + public void onDialogHmsSet(int ref, int hours, int minutes, int seconds) { + if (controller != null && controller.serviceAvailable()) { + controller.setSleepTimer((hours * 3600 + minutes * 60 + seconds) * 1000); + } + } + }); + hpb.show(); + } else { + TimeDialog td = new TimeDialog(this, + R.string.set_sleeptimer_label, + R.string.set_sleeptimer_label) { + + @Override + public void onTimeEntered(long millis) { + controller.setSleepTimer(millis); + } + }; + td.show(); + } break; } diff --git a/src/de/danoeh/antennapod/activity/OnlineFeedViewActivity.java b/src/de/danoeh/antennapod/activity/OnlineFeedViewActivity.java index e397ff2ca..2c6d75cd8 100644 --- a/src/de/danoeh/antennapod/activity/OnlineFeedViewActivity.java +++ b/src/de/danoeh/antennapod/activity/OnlineFeedViewActivity.java @@ -9,6 +9,7 @@ import android.content.Intent; import android.os.Bundle; import android.support.v7.app.ActionBarActivity; import android.util.Log; +import android.widget.ArrayAdapter; import android.widget.LinearLayout; import android.widget.ProgressBar; import android.widget.RelativeLayout; @@ -29,13 +30,16 @@ 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 de.danoeh.antennapod.util.syndication.FeedDiscoverer; import org.apache.commons.lang3.StringUtils; import org.xml.sax.SAXException; import javax.xml.parsers.ParserConfigurationException; import java.io.File; import java.io.IOException; +import java.util.ArrayList; import java.util.Date; +import java.util.List; import java.util.Map; /** @@ -127,6 +131,13 @@ public abstract class OnlineFeedViewActivity extends ActionBarActivity { } } + private void resetIntent(String url, String title) { + Intent intent = new Intent(); + intent.putExtra(ARG_FEEDURL, url); + intent.putExtra(ARG_TITLE, title); + setIntent(intent); + } + private void onDownloadCompleted(final Downloader downloader) { runOnUiThread(new Runnable() { @@ -244,8 +255,15 @@ public abstract class OnlineFeedViewActivity extends ActionBarActivity { e.printStackTrace(); reasonDetailed = e.getMessage(); } catch (UnsupportedFeedtypeException e) { - e.printStackTrace(); - reasonDetailed = e.getMessage(); + if (BuildConfig.DEBUG) Log.d(TAG, "Unsupported feed type detected"); + if (StringUtils.equalsIgnoreCase("html", e.getRootElement())) { + if (showFeedDiscoveryDialog(new File(feed.getFile_url()), feed.getDownload_url())) { + return; + } + } else { + e.printStackTrace(); + reasonDetailed = e.getMessage(); + } } finally { boolean rc = new File(feed.getFile_url()).delete(); if (BuildConfig.DEBUG) @@ -253,6 +271,7 @@ public abstract class OnlineFeedViewActivity extends ActionBarActivity { } if (successful) { + beforeShowFeedInformation(feed, alternateFeedUrls); runOnUiThread(new Runnable() { @Override public void run() { @@ -285,7 +304,16 @@ public abstract class OnlineFeedViewActivity extends ActionBarActivity { } /** - * Called when feed parsed successfully + * Called after the feed has been downloaded and parsed and before showFeedInformation is called. + * This method is executed on a background thread + */ + protected void beforeShowFeedInformation(Feed feed, Map<String, String> alternateFeedUrls) { + + } + + /** + * Called when feed parsed successfully. + * This method is executed on the GUI thread. */ protected void showFeedInformation(Feed feed, Map<String, String> alternateFeedUrls) { @@ -316,9 +344,67 @@ public abstract class OnlineFeedViewActivity extends ActionBarActivity { finish(); } }); + builder.show(); } } + private boolean showFeedDiscoveryDialog(File feedFile, String baseUrl) { + FeedDiscoverer fd = new FeedDiscoverer(); + final Map<String, String> urlsMap; + try { + urlsMap = fd.findLinks(feedFile, baseUrl); + if (urlsMap == null || urlsMap.isEmpty()) { + return false; + } + } catch (IOException e) { + e.printStackTrace(); + return false; + } + + runOnUiThread(new Runnable() { + @Override + public void run() { + if (isPaused || isFinishing()) { + return; + } + + final List<String> titles = new ArrayList<String>(); + final List<String> urls = new ArrayList<String>(); + + urls.addAll(urlsMap.keySet()); + for (String url : urls) { + titles.add(urlsMap.get(url)); + } + + final ArrayAdapter<String> adapter = new ArrayAdapter<String>(OnlineFeedViewActivity.this, R.layout.ellipsize_start_listitem, R.id.txtvTitle, titles); + DialogInterface.OnClickListener onClickListener = new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + String selectedUrl = urls.get(which); + dialog.dismiss(); + resetIntent(selectedUrl, titles.get(which)); + startFeedDownload(selectedUrl, null, null); + } + }; + + AlertDialog.Builder ab = new AlertDialog.Builder(OnlineFeedViewActivity.this) + .setTitle(R.string.feeds_label) + .setCancelable(true) + .setOnCancelListener(new OnCancelListener() { + @Override + public void onCancel(DialogInterface dialog) { + finish(); + } + }) + .setAdapter(adapter, onClickListener); + ab.show(); + } + }); + + + return true; + } + private class FeedViewAuthenticationDialog extends AuthenticationDialog { private String feedUrl; @@ -339,5 +425,4 @@ public abstract class OnlineFeedViewActivity extends ActionBarActivity { startFeedDownload(feedUrl, username, password); } } - } diff --git a/src/de/danoeh/antennapod/activity/OpmlFeedChooserActivity.java b/src/de/danoeh/antennapod/activity/OpmlFeedChooserActivity.java index 5f2ea0401..e09941abf 100644 --- a/src/de/danoeh/antennapod/activity/OpmlFeedChooserActivity.java +++ b/src/de/danoeh/antennapod/activity/OpmlFeedChooserActivity.java @@ -103,11 +103,11 @@ public class OpmlFeedChooserActivity extends ActionBarActivity { super.onCreateOptionsMenu(menu); MenuItemCompat.setShowAsAction(menu.add(Menu.NONE, R.id.select_all_item, Menu.NONE, R.string.select_all_label), - MenuItem.SHOW_AS_ACTION_IF_ROOM); + MenuItemCompat.SHOW_AS_ACTION_IF_ROOM); MenuItemCompat.setShowAsAction(menu.add(Menu.NONE, R.id.deselect_all_item, Menu.NONE, R.string.deselect_all_label), - MenuItem.SHOW_AS_ACTION_IF_ROOM); + MenuItemCompat.SHOW_AS_ACTION_IF_ROOM); return true; } diff --git a/src/de/danoeh/antennapod/activity/PreferenceActivity.java b/src/de/danoeh/antennapod/activity/PreferenceActivity.java index 77ec579ed..a62ad3ae9 100644 --- a/src/de/danoeh/antennapod/activity/PreferenceActivity.java +++ b/src/de/danoeh/antennapod/activity/PreferenceActivity.java @@ -15,7 +15,6 @@ import android.preference.Preference; import android.preference.Preference.OnPreferenceChangeListener; import android.preference.Preference.OnPreferenceClickListener; import android.preference.PreferenceScreen; -import android.support.v4.app.NavUtils; import android.util.Log; import android.view.Menu; import android.view.MenuItem; @@ -25,6 +24,7 @@ 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.AutoFlattrPreferenceDialog; import de.danoeh.antennapod.dialog.GpodnetSetHostnameDialog; import de.danoeh.antennapod.dialog.VariableSpeedDialog; import de.danoeh.antennapod.preferences.GpodnetPreferences; @@ -47,7 +47,7 @@ public class PreferenceActivity extends android.preference.PreferenceActivity { private static final String PREF_FLATTR_THIS_APP = "prefFlattrThisApp"; private static final String PREF_FLATTR_AUTH = "pref_flattr_authenticate"; private static final String PREF_FLATTR_REVOKE = "prefRevokeAccess"; - private static final String PREF_AUTO_FLATTR = "pref_auto_flattr"; + private static final String PREF_AUTO_FLATTR_PREFS = "prefAutoFlattrPrefs"; private static final String PREF_OPML_EXPORT = "prefOpmlExport"; private static final String PREF_ABOUT = "prefAbout"; private static final String PREF_CHOOSE_DATA_DIR = "prefChooseDataDir"; @@ -69,7 +69,7 @@ public class PreferenceActivity extends android.preference.PreferenceActivity { super.onCreate(savedInstanceState); if (android.os.Build.VERSION.SDK_INT >= 11) { - ActionBar ab = getActionBar(); + @SuppressLint("AppCompatMethod") ActionBar ab = getActionBar(); if (ab != null) { ab.setDisplayHomeAsUpEnabled(true); } @@ -247,6 +247,26 @@ public class PreferenceActivity extends android.preference.PreferenceActivity { return true; } }); + + findPreference(PREF_AUTO_FLATTR_PREFS).setOnPreferenceClickListener(new OnPreferenceClickListener() { + @Override + public boolean onPreferenceClick(Preference preference) { + AutoFlattrPreferenceDialog.newAutoFlattrPreferenceDialog(PreferenceActivity.this, + new AutoFlattrPreferenceDialog.AutoFlattrPreferenceDialogInterface() { + @Override + public void onCancelled() { + + } + + @Override + public void onConfirmed(boolean autoFlattrEnabled, float autoFlattrValue) { + UserPreferences.setAutoFlattrSettings(PreferenceActivity.this, autoFlattrEnabled, autoFlattrValue); + checkItemVisibility(); + } + }); + return true; + } + }); buildUpdateIntervalPreference(); buildAutodownloadSelectedNetworsPreference(); setSelectedNetworksEnabled(UserPreferences @@ -314,7 +334,7 @@ public class PreferenceActivity extends android.preference.PreferenceActivity { findPreference(PREF_FLATTR_AUTH).setEnabled(!hasFlattrToken); findPreference(PREF_FLATTR_REVOKE).setEnabled(hasFlattrToken); - findPreference(PREF_AUTO_FLATTR).setEnabled(hasFlattrToken); + findPreference(PREF_AUTO_FLATTR_PREFS).setEnabled(hasFlattrToken); findPreference(UserPreferences.PREF_ENABLE_AUTODL_WIFI_FILTER) .setEnabled(UserPreferences.isEnableAutodownload()); diff --git a/src/de/danoeh/antennapod/activity/StorageErrorActivity.java b/src/de/danoeh/antennapod/activity/StorageErrorActivity.java index 2cd56ba8b..d8a137eb9 100644 --- a/src/de/danoeh/antennapod/activity/StorageErrorActivity.java +++ b/src/de/danoeh/antennapod/activity/StorageErrorActivity.java @@ -7,6 +7,9 @@ import android.content.IntentFilter; import android.os.Bundle; import android.support.v7.app.ActionBarActivity; import android.util.Log; + +import org.apache.commons.lang3.StringUtils; + import de.danoeh.antennapod.BuildConfig; import de.danoeh.antennapod.R; import de.danoeh.antennapod.preferences.UserPreferences; @@ -54,7 +57,7 @@ public class StorageErrorActivity extends ActionBarActivity { @Override public void onReceive(Context context, Intent intent) { - if (intent.getAction().equals(Intent.ACTION_MEDIA_MOUNTED)) { + if (StringUtils.equals(intent.getAction(), Intent.ACTION_MEDIA_MOUNTED)) { if (intent.getBooleanExtra("read-only", true)) { if (BuildConfig.DEBUG) Log.d(TAG, "Media was mounted; Finishing activity"); diff --git a/src/de/danoeh/antennapod/activity/VideoplayerActivity.java b/src/de/danoeh/antennapod/activity/VideoplayerActivity.java index c45a3d162..46fa98c49 100644 --- a/src/de/danoeh/antennapod/activity/VideoplayerActivity.java +++ b/src/de/danoeh/antennapod/activity/VideoplayerActivity.java @@ -6,14 +6,19 @@ import android.graphics.drawable.ColorDrawable; import android.os.AsyncTask; import android.os.Build; import android.os.Bundle; +import android.support.v4.view.WindowCompat; import android.util.Log; import android.util.Pair; -import android.view.*; +import android.view.MotionEvent; +import android.view.SurfaceHolder; +import android.view.View; +import android.view.WindowManager; import android.view.animation.Animation; import android.view.animation.AnimationUtils; import android.widget.LinearLayout; import android.widget.ProgressBar; import android.widget.SeekBar; + import de.danoeh.antennapod.BuildConfig; import de.danoeh.antennapod.R; import de.danoeh.antennapod.feed.MediaType; @@ -48,7 +53,7 @@ public class VideoplayerActivity extends MediaplayerActivity { @Override protected void onCreate(Bundle savedInstanceState) { if (Build.VERSION.SDK_INT >= 11) { - requestWindowFeature(Window.FEATURE_ACTION_BAR_OVERLAY); + supportRequestWindowFeature(WindowCompat.FEATURE_ACTION_BAR_OVERLAY); } getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); super.onCreate(savedInstanceState); @@ -126,7 +131,8 @@ public class VideoplayerActivity extends MediaplayerActivity { Pair<Integer, Integer> videoSize = controller.getVideoSize(); if (videoSize != null && videoSize.first > 0 && videoSize.second > 0) { - if (BuildConfig.DEBUG) Log.d(TAG, "Width,height of video: " + videoSize.first + ", " + videoSize.second); + if (BuildConfig.DEBUG) + Log.d(TAG, "Width,height of video: " + videoSize.first + ", " + videoSize.second); videoview.setVideoSize(videoSize.first, videoSize.second); } else { Log.e(TAG, "Could not determine video size"); @@ -304,6 +310,7 @@ public class VideoplayerActivity extends MediaplayerActivity { progressIndicator.setVisibility(View.INVISIBLE); } + @SuppressLint("NewApi") private void showVideoControls() { videoOverlay.setVisibility(View.VISIBLE); butPlay.setVisibility(View.VISIBLE); @@ -318,6 +325,7 @@ public class VideoplayerActivity extends MediaplayerActivity { } } + @SuppressLint("NewApi") private void hideVideoControls() { final Animation animation = AnimationUtils.loadAnimation(this, R.anim.fade_out); diff --git a/src/de/danoeh/antennapod/activity/gpoddernet/GpodnetAuthenticationActivity.java b/src/de/danoeh/antennapod/activity/gpoddernet/GpodnetAuthenticationActivity.java index 05048f079..cb6dc41cf 100644 --- a/src/de/danoeh/antennapod/activity/gpoddernet/GpodnetAuthenticationActivity.java +++ b/src/de/danoeh/antennapod/activity/gpoddernet/GpodnetAuthenticationActivity.java @@ -67,9 +67,9 @@ public class GpodnetAuthenticationActivity extends ActionBarActivity { 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) + inflater.inflate(R.layout.gpodnetauth_credentials, viewFlipper, false), + inflater.inflate(R.layout.gpodnetauth_device, viewFlipper, false), + inflater.inflate(R.layout.gpodnetauth_finish, viewFlipper, false) }; for (View view : views) { viewFlipper.addView(view); diff --git a/src/de/danoeh/antennapod/adapter/ActionButtonUtils.java b/src/de/danoeh/antennapod/adapter/ActionButtonUtils.java index 17c61a86c..1de071a73 100644 --- a/src/de/danoeh/antennapod/adapter/ActionButtonUtils.java +++ b/src/de/danoeh/antennapod/adapter/ActionButtonUtils.java @@ -4,6 +4,9 @@ import android.content.Context; import android.content.res.TypedArray; import android.view.View; import android.widget.ImageButton; + +import org.apache.commons.lang3.Validate; + import de.danoeh.antennapod.R; import de.danoeh.antennapod.feed.FeedItem; import de.danoeh.antennapod.feed.FeedMedia; @@ -20,11 +23,12 @@ public class ActionButtonUtils { private final Context context; public ActionButtonUtils(Context context) { - if (context == null) throw new IllegalArgumentException("context = null"); + Validate.notNull(context); + this.context = context; drawables = context.obtainStyledAttributes(new int[]{ - R.attr.av_play, R.attr.navigation_cancel, R.attr.av_download, R.attr.navigation_chapters}); - labels = new int[]{R.string.play_label, R.string.cancel_download_label, R.string.download_label}; + R.attr.av_play, R.attr.navigation_cancel, R.attr.av_download, R.attr.navigation_chapters, R.attr.navigation_accept}); + labels = new int[]{R.string.play_label, R.string.cancel_download_label, R.string.download_label, R.string.mark_read_label}; } /** @@ -32,7 +36,8 @@ public class ActionButtonUtils { * action button so that it matches the state of the FeedItem. */ public void configureActionButton(ImageButton butSecondary, FeedItem item) { - if (butSecondary == null || item == null) throw new IllegalArgumentException("butSecondary or item was null"); + Validate.isTrue(butSecondary != null && item != null, "butSecondary or item was null"); + final FeedMedia media = item.getMedia(); if (media != null) { final boolean isDownloadingMedia = DownloadRequester.getInstance().isDownloadingFile(media); @@ -61,7 +66,13 @@ public class ActionButtonUtils { butSecondary.setContentDescription(context.getString(labels[0])); } } else { - butSecondary.setVisibility(View.INVISIBLE); + if (item.isRead()) { + butSecondary.setVisibility(View.INVISIBLE); + } else { + butSecondary.setVisibility(View.VISIBLE); + butSecondary.setImageDrawable(drawables.getDrawable(4)); + butSecondary.setContentDescription(context.getString(labels[3])); + } } } } diff --git a/src/de/danoeh/antennapod/adapter/ChapterListAdapter.java b/src/de/danoeh/antennapod/adapter/ChapterListAdapter.java index 72ad6774f..c12de6ebd 100644 --- a/src/de/danoeh/antennapod/adapter/ChapterListAdapter.java +++ b/src/de/danoeh/antennapod/adapter/ChapterListAdapter.java @@ -51,7 +51,7 @@ public class ChapterListAdapter extends ArrayAdapter<Chapter> { LayoutInflater inflater = (LayoutInflater) getContext() .getSystemService(Context.LAYOUT_INFLATER_SERVICE); - convertView = inflater.inflate(R.layout.simplechapter_item, null); + convertView = inflater.inflate(R.layout.simplechapter_item, parent, false); holder.title = (TextView) convertView.findViewById(R.id.txtvTitle); defaultTextColor = holder.title.getTextColors().getDefaultColor(); holder.start = (TextView) convertView.findViewById(R.id.txtvStart); diff --git a/src/de/danoeh/antennapod/adapter/DefaultActionButtonCallback.java b/src/de/danoeh/antennapod/adapter/DefaultActionButtonCallback.java index 3acd587af..0c4cbe685 100644 --- a/src/de/danoeh/antennapod/adapter/DefaultActionButtonCallback.java +++ b/src/de/danoeh/antennapod/adapter/DefaultActionButtonCallback.java @@ -2,11 +2,15 @@ package de.danoeh.antennapod.adapter; import android.content.Context; import android.widget.Toast; + +import org.apache.commons.lang3.Validate; + import de.danoeh.antennapod.R; import de.danoeh.antennapod.dialog.DownloadRequestErrorDialogCreator; import de.danoeh.antennapod.feed.FeedItem; import de.danoeh.antennapod.feed.FeedMedia; import de.danoeh.antennapod.storage.DBTasks; +import de.danoeh.antennapod.storage.DBWriter; import de.danoeh.antennapod.storage.DownloadRequestException; import de.danoeh.antennapod.storage.DownloadRequester; @@ -19,31 +23,35 @@ public class DefaultActionButtonCallback implements ActionButtonCallback { private final Context context; public DefaultActionButtonCallback(Context context) { - if (context == null) throw new IllegalArgumentException("context = null"); + Validate.notNull(context); this.context = context; } @Override public void onActionButtonPressed(final FeedItem item) { - final FeedMedia media = item.getMedia(); - if (media == null) { - return; - } - boolean isDownloading = DownloadRequester.getInstance().isDownloadingFile(media); - if (!isDownloading && !media.isDownloaded()) { - try { - DBTasks.downloadFeedItems(context, item); - Toast.makeText(context, R.string.status_downloading_label, Toast.LENGTH_SHORT).show(); - } catch (DownloadRequestException e) { - e.printStackTrace(); - DownloadRequestErrorDialogCreator.newRequestErrorDialog(context, e.getMessage()); + + if (item.hasMedia()) { + final FeedMedia media = item.getMedia(); + boolean isDownloading = DownloadRequester.getInstance().isDownloadingFile(media); + if (!isDownloading && !media.isDownloaded()) { + try { + DBTasks.downloadFeedItems(context, item); + Toast.makeText(context, R.string.status_downloading_label, Toast.LENGTH_SHORT).show(); + } catch (DownloadRequestException e) { + e.printStackTrace(); + DownloadRequestErrorDialogCreator.newRequestErrorDialog(context, e.getMessage()); + } + } else if (isDownloading) { + DownloadRequester.getInstance().cancelDownload(context, media); + Toast.makeText(context, R.string.download_cancelled_msg, Toast.LENGTH_SHORT).show(); + } else { // media is downloaded + DBTasks.playMedia(context, media, true, true, false); + } + } else { + if (!item.isRead()) { + DBWriter.markItemRead(context, item, true, true); } - } else if (isDownloading) { - DownloadRequester.getInstance().cancelDownload(context, media); - Toast.makeText(context, R.string.download_cancelled_msg, Toast.LENGTH_SHORT).show(); - } else { // media is downloaded - DBTasks.playMedia(context, media, true, true, false); } } } diff --git a/src/de/danoeh/antennapod/adapter/DownloadLogAdapter.java b/src/de/danoeh/antennapod/adapter/DownloadLogAdapter.java index e97d69494..2cc216227 100644 --- a/src/de/danoeh/antennapod/adapter/DownloadLogAdapter.java +++ b/src/de/danoeh/antennapod/adapter/DownloadLogAdapter.java @@ -34,7 +34,7 @@ public class DownloadLogAdapter extends BaseAdapter { holder = new Holder(); LayoutInflater inflater = (LayoutInflater) context .getSystemService(Context.LAYOUT_INFLATER_SERVICE); - convertView = inflater.inflate(R.layout.downloadlog_item, null); + convertView = inflater.inflate(R.layout.downloadlog_item, parent, false); holder.title = (TextView) convertView.findViewById(R.id.txtvTitle); holder.type = (TextView) convertView.findViewById(R.id.txtvType); holder.date = (TextView) convertView.findViewById(R.id.txtvDate); diff --git a/src/de/danoeh/antennapod/adapter/DownloadedEpisodesListAdapter.java b/src/de/danoeh/antennapod/adapter/DownloadedEpisodesListAdapter.java index 0bf770df2..33b11774f 100644 --- a/src/de/danoeh/antennapod/adapter/DownloadedEpisodesListAdapter.java +++ b/src/de/danoeh/antennapod/adapter/DownloadedEpisodesListAdapter.java @@ -54,7 +54,7 @@ public class DownloadedEpisodesListAdapter extends BaseAdapter { LayoutInflater inflater = (LayoutInflater) context .getSystemService(Context.LAYOUT_INFLATER_SERVICE); convertView = inflater.inflate(R.layout.downloaded_episodeslist_item, - null); + parent, false); holder.title = (TextView) convertView.findViewById(R.id.txtvTitle); holder.pubDate = (TextView) convertView .findViewById(R.id.txtvPublished); diff --git a/src/de/danoeh/antennapod/adapter/DownloadlistAdapter.java b/src/de/danoeh/antennapod/adapter/DownloadlistAdapter.java index fa2e5a0a7..658af9e4e 100644 --- a/src/de/danoeh/antennapod/adapter/DownloadlistAdapter.java +++ b/src/de/danoeh/antennapod/adapter/DownloadlistAdapter.java @@ -56,7 +56,7 @@ public class DownloadlistAdapter extends BaseAdapter { holder = new Holder(); LayoutInflater inflater = (LayoutInflater) context .getSystemService(Context.LAYOUT_INFLATER_SERVICE); - convertView = inflater.inflate(R.layout.downloadlist_item, null); + convertView = inflater.inflate(R.layout.downloadlist_item, parent, false); holder.title = (TextView) convertView.findViewById(R.id.txtvTitle); holder.message = (TextView) convertView .findViewById(R.id.txtvMessage); diff --git a/src/de/danoeh/antennapod/adapter/ExternalEpisodesListAdapter.java b/src/de/danoeh/antennapod/adapter/ExternalEpisodesListAdapter.java index 9a7b607aa..5e857c131 100644 --- a/src/de/danoeh/antennapod/adapter/ExternalEpisodesListAdapter.java +++ b/src/de/danoeh/antennapod/adapter/ExternalEpisodesListAdapter.java @@ -72,7 +72,7 @@ public class ExternalEpisodesListAdapter extends BaseExpandableListAdapter { LayoutInflater inflater = (LayoutInflater) context .getSystemService(Context.LAYOUT_INFLATER_SERVICE); convertView = inflater.inflate(R.layout.external_itemlist_item, - null); + parent, false); holder.title = (TextView) convertView.findViewById(R.id.txtvTitle); holder.feedTitle = (TextView) convertView .findViewById(R.id.txtvFeedname); @@ -225,7 +225,7 @@ public class ExternalEpisodesListAdapter extends BaseExpandableListAdapter { View convertView, ViewGroup parent) { LayoutInflater inflater = (LayoutInflater) context .getSystemService(Context.LAYOUT_INFLATER_SERVICE); - convertView = inflater.inflate(R.layout.feeditemlist_header, null); + convertView = inflater.inflate(R.layout.feeditemlist_header, parent, false); TextView headerTitle = (TextView) convertView .findViewById(0); ImageButton actionButton = (ImageButton) convertView diff --git a/src/de/danoeh/antennapod/adapter/FeedItemlistAdapter.java b/src/de/danoeh/antennapod/adapter/FeedItemlistAdapter.java index c4a16d4db..357b5f8b4 100644 --- a/src/de/danoeh/antennapod/adapter/FeedItemlistAdapter.java +++ b/src/de/danoeh/antennapod/adapter/FeedItemlistAdapter.java @@ -67,7 +67,7 @@ public class FeedItemlistAdapter extends BaseAdapter { holder = new Holder(); LayoutInflater inflater = (LayoutInflater) context .getSystemService(Context.LAYOUT_INFLATER_SERVICE); - convertView = inflater.inflate(R.layout.feeditemlist_item, null); + convertView = inflater.inflate(R.layout.feeditemlist_item, parent, false); holder.title = (TextView) convertView .findViewById(R.id.txtvItemname); holder.lenSize = (TextView) convertView diff --git a/src/de/danoeh/antennapod/adapter/FeedItemlistDescriptionAdapter.java b/src/de/danoeh/antennapod/adapter/FeedItemlistDescriptionAdapter.java index 5fb204b26..c2c2285ac 100644 --- a/src/de/danoeh/antennapod/adapter/FeedItemlistDescriptionAdapter.java +++ b/src/de/danoeh/antennapod/adapter/FeedItemlistDescriptionAdapter.java @@ -12,7 +12,7 @@ import de.danoeh.antennapod.feed.FeedItem; import java.util.List; /** - * Created by daniel on 24.08.13. + * List adapter for showing a list of FeedItems with their title and description. */ public class FeedItemlistDescriptionAdapter extends ArrayAdapter<FeedItem> { @@ -31,7 +31,7 @@ public class FeedItemlistDescriptionAdapter extends ArrayAdapter<FeedItem> { holder = new Holder(); LayoutInflater inflater = (LayoutInflater) getContext() .getSystemService(Context.LAYOUT_INFLATER_SERVICE); - convertView = inflater.inflate(R.layout.itemdescription_listitem, null); + convertView = inflater.inflate(R.layout.itemdescription_listitem, parent, false); holder.title = (TextView) convertView.findViewById(R.id.txtvTitle); holder.description = (TextView) convertView.findViewById(R.id.txtvDescription); diff --git a/src/de/danoeh/antennapod/adapter/NavListAdapter.java b/src/de/danoeh/antennapod/adapter/NavListAdapter.java index 536bf80e3..9676372fb 100644 --- a/src/de/danoeh/antennapod/adapter/NavListAdapter.java +++ b/src/de/danoeh/antennapod/adapter/NavListAdapter.java @@ -110,7 +110,7 @@ public class NavListAdapter extends BaseAdapter { LayoutInflater inflater = (LayoutInflater) context .getSystemService(Context.LAYOUT_INFLATER_SERVICE); - convertView = inflater.inflate(R.layout.nav_listitem, null); + convertView = inflater.inflate(R.layout.nav_listitem, parent, false); holder.title = (TextView) convertView.findViewById(R.id.txtvTitle); holder.count = (TextView) convertView.findViewById(R.id.txtvCount); @@ -154,7 +154,7 @@ public class NavListAdapter extends BaseAdapter { LayoutInflater inflater = (LayoutInflater) context .getSystemService(Context.LAYOUT_INFLATER_SERVICE); - convertView = inflater.inflate(R.layout.nav_section_item, null); + convertView = inflater.inflate(R.layout.nav_section_item, parent, false); holder.title = (TextView) convertView.findViewById(R.id.txtvTitle); convertView.setTag(holder); @@ -179,7 +179,7 @@ public class NavListAdapter extends BaseAdapter { LayoutInflater inflater = (LayoutInflater) context .getSystemService(Context.LAYOUT_INFLATER_SERVICE); - convertView = inflater.inflate(R.layout.nav_feedlistitem, null); + convertView = inflater.inflate(R.layout.nav_feedlistitem, parent, false); holder.title = (TextView) convertView.findViewById(R.id.txtvTitle); holder.image = (ImageView) convertView.findViewById(R.id.imgvCover); diff --git a/src/de/danoeh/antennapod/adapter/NewEpisodesListAdapter.java b/src/de/danoeh/antennapod/adapter/NewEpisodesListAdapter.java index 555a334f6..07fd3e6b1 100644 --- a/src/de/danoeh/antennapod/adapter/NewEpisodesListAdapter.java +++ b/src/de/danoeh/antennapod/adapter/NewEpisodesListAdapter.java @@ -62,7 +62,7 @@ public class NewEpisodesListAdapter extends BaseAdapter { LayoutInflater inflater = (LayoutInflater) context .getSystemService(Context.LAYOUT_INFLATER_SERVICE); convertView = inflater.inflate(R.layout.new_episodes_listitem, - null); + parent, false); holder.title = (TextView) convertView.findViewById(R.id.txtvTitle); holder.pubDate = (TextView) convertView .findViewById(R.id.txtvPublished); diff --git a/src/de/danoeh/antennapod/adapter/QueueListAdapter.java b/src/de/danoeh/antennapod/adapter/QueueListAdapter.java index ecce1b473..f671ba5c6 100644 --- a/src/de/danoeh/antennapod/adapter/QueueListAdapter.java +++ b/src/de/danoeh/antennapod/adapter/QueueListAdapter.java @@ -57,7 +57,7 @@ public class QueueListAdapter extends BaseAdapter { LayoutInflater inflater = (LayoutInflater) context .getSystemService(Context.LAYOUT_INFLATER_SERVICE); convertView = inflater.inflate(R.layout.queue_listitem, - null); + parent, false); holder.title = (TextView) convertView.findViewById(R.id.txtvTitle); holder.butSecondary = (ImageButton) convertView .findViewById(R.id.butSecondaryAction); diff --git a/src/de/danoeh/antennapod/adapter/SearchlistAdapter.java b/src/de/danoeh/antennapod/adapter/SearchlistAdapter.java index 5c6af3943..ecfbb4660 100644 --- a/src/de/danoeh/antennapod/adapter/SearchlistAdapter.java +++ b/src/de/danoeh/antennapod/adapter/SearchlistAdapter.java @@ -55,7 +55,7 @@ public class SearchlistAdapter extends BaseAdapter { LayoutInflater inflater = (LayoutInflater) context .getSystemService(Context.LAYOUT_INFLATER_SERVICE); - convertView = inflater.inflate(R.layout.searchlist_item, null); + convertView = inflater.inflate(R.layout.searchlist_item, parent, false); holder.title = (TextView) convertView.findViewById(R.id.txtvTitle); holder.cover = (ImageView) convertView .findViewById(R.id.imgvFeedimage); diff --git a/src/de/danoeh/antennapod/adapter/gpodnet/PodcastListAdapter.java b/src/de/danoeh/antennapod/adapter/gpodnet/PodcastListAdapter.java index 795b17917..f20232a6f 100644 --- a/src/de/danoeh/antennapod/adapter/gpodnet/PodcastListAdapter.java +++ b/src/de/danoeh/antennapod/adapter/gpodnet/PodcastListAdapter.java @@ -38,7 +38,7 @@ public class PodcastListAdapter extends ArrayAdapter<GpodnetPodcast> { LayoutInflater inflater = (LayoutInflater) getContext() .getSystemService(Context.LAYOUT_INFLATER_SERVICE); - convertView = inflater.inflate(R.layout.gpodnet_podcast_listitem, null); + convertView = inflater.inflate(R.layout.gpodnet_podcast_listitem, parent, false); holder.title = (TextView) convertView.findViewById(R.id.txtvTitle); holder.description = (TextView) convertView.findViewById(R.id.txtvDescription); holder.image = (ImageView) convertView.findViewById(R.id.imgvCover); diff --git a/src/de/danoeh/antennapod/asynctask/DownloadObserver.java b/src/de/danoeh/antennapod/asynctask/DownloadObserver.java index 1c5003ab3..21ae5291e 100644 --- a/src/de/danoeh/antennapod/asynctask/DownloadObserver.java +++ b/src/de/danoeh/antennapod/asynctask/DownloadObserver.java @@ -5,6 +5,9 @@ import android.content.*; import android.os.Handler; import android.os.IBinder; import android.util.Log; + +import org.apache.commons.lang3.Validate; + import de.danoeh.antennapod.BuildConfig; import de.danoeh.antennapod.service.download.DownloadService; import de.danoeh.antennapod.service.download.Downloader; @@ -45,9 +48,9 @@ public class DownloadObserver { * @throws java.lang.IllegalArgumentException if one of the arguments is null. */ public DownloadObserver(Activity activity, Handler handler, Callback callback) { - if (activity == null) throw new IllegalArgumentException("activity = null"); - if (handler == null) throw new IllegalArgumentException("handler = null"); - if (callback == null) throw new IllegalArgumentException("callback = null"); + Validate.notNull(activity); + Validate.notNull(handler); + Validate.notNull(callback); this.activity = activity; this.handler = handler; @@ -166,7 +169,7 @@ public class DownloadObserver { } public void setActivity(Activity activity) { - if (activity == null) throw new IllegalArgumentException("activity = null"); + Validate.notNull(activity); this.activity = activity; } diff --git a/src/de/danoeh/antennapod/asynctask/FlattrClickWorker.java b/src/de/danoeh/antennapod/asynctask/FlattrClickWorker.java index e9aa79ac1..9210ac1d1 100644 --- a/src/de/danoeh/antennapod/asynctask/FlattrClickWorker.java +++ b/src/de/danoeh/antennapod/asynctask/FlattrClickWorker.java @@ -1,300 +1,236 @@ package de.danoeh.antennapod.asynctask; -import android.annotation.SuppressLint; +import android.annotation.TargetApi; import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; -import android.app.ProgressDialog; import android.content.Context; import android.content.Intent; -import android.net.ConnectivityManager; -import android.net.NetworkInfo; import android.os.AsyncTask; import android.support.v4.app.NotificationCompat; import android.util.Log; import android.widget.Toast; + +import org.apache.commons.lang3.Validate; +import org.shredzone.flattr4j.exception.FlattrException; + +import java.util.LinkedList; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; + import de.danoeh.antennapod.BuildConfig; import de.danoeh.antennapod.R; +import de.danoeh.antennapod.activity.FlattrAuthActivity; import de.danoeh.antennapod.activity.MainActivity; import de.danoeh.antennapod.storage.DBReader; import de.danoeh.antennapod.storage.DBWriter; +import de.danoeh.antennapod.util.NetworkUtils; import de.danoeh.antennapod.util.flattr.FlattrThing; import de.danoeh.antennapod.util.flattr.FlattrUtils; -import java.util.ArrayList; -import java.util.List; - /** * Performs a click action in a background thread. + * <p/> + * When started, the flattr click worker will try to flattr every item that is in the flattr queue. If no network + * connection is available it will shut down immediately. The FlattrClickWorker can also be given one additional + * FlattrThing which will be flattrd immediately. + * <p/> + * The FlattrClickWorker will display a toast notification for every item that has been flattrd. If the FlattrClickWorker failed + * to flattr something, a notification will be displayed. */ - -public class FlattrClickWorker extends AsyncTask<Void, String, Void> { +public class FlattrClickWorker extends AsyncTask<Void, Integer, FlattrClickWorker.ExitCode> { protected static final String TAG = "FlattrClickWorker"; - protected Context context; - - private final int NOTIFICATION_ID = 4; - protected String errorMsg; - protected int exitCode; - protected ArrayList<String> flattrd; - protected ArrayList<String> flattr_failed; + private static final int NOTIFICATION_ID = 4; + private final Context context; - protected NotificationCompat.Builder notificationCompatBuilder; - private Notification.BigTextStyle notificationBuilder; - protected NotificationManager notificationManager; + public static enum ExitCode {EXIT_NORMAL, NO_TOKEN, NO_NETWORK, NO_THINGS} - protected ProgressDialog progDialog; + private volatile int countFailed = 0; + private volatile int countSuccess = 0; - protected final static int EXIT_DEFAULT = 0; - protected final static int NO_TOKEN = 1; - protected final static int ENQUEUED = 2; - protected final static int NO_THINGS = 3; - - public final static int ENQUEUE_ONLY = 1; - public final static int FLATTR_TOAST = 2; - public static final int FLATTR_NOTIFICATION = 3; - - private int run_mode = FLATTR_NOTIFICATION; - - private FlattrThing extra_flattr_thing; // additional urls to flattr that do *not* originate from the queue + private volatile FlattrThing extraFlattrThing; /** - * @param context - * @param run_mode can be one of ENQUEUE_ONLY, FLATTR_TOAST and FLATTR_NOTIFICATION + * Only relevant if just one thing is flattrd */ - public FlattrClickWorker(Context context, int run_mode) { - this(context); - this.run_mode = run_mode; - } + private volatile FlattrException exception; + /** + * Creates a new FlattrClickWorker which will only flattr all things in the queue. + * <p/> + * The FlattrClickWorker has to be started by calling executeAsync(). + * + * @param context A context for accessing the database and posting notifications. Must not be null. + */ public FlattrClickWorker(Context context) { - super(); - this.context = context; - exitCode = EXIT_DEFAULT; - - flattrd = new ArrayList<String>(); - flattr_failed = new ArrayList<String>(); - - errorMsg = ""; + Validate.notNull(context); + this.context = context.getApplicationContext(); } - /* only used in PreferencesActivity for flattring antennapod itself, - * can't really enqueue this thing - */ - public FlattrClickWorker(Context context, FlattrThing thing) { + /** + * Creates a new FlattrClickWorker which will flattr all things in the queue and one additional + * FlattrThing. + * <p/> + * The FlattrClickWorker has to be started by calling executeAsync(). + * + * @param context A context for accessing the database and posting notifications. Must not be null. + * @param extraFlattrThing The additional thing to flattr + */ + public FlattrClickWorker(Context context, FlattrThing extraFlattrThing) { this(context); - extra_flattr_thing = thing; - run_mode = FLATTR_TOAST; - Log.d(TAG, "Going to flattr special thing that is not in the queue: " + thing.getTitle()); - } - - protected void onNoAccessToken() { - Log.w(TAG, "No access token was available"); - } - - protected void onFlattrError() { - FlattrUtils.showErrorDialog(context, errorMsg); + this.extraFlattrThing = extraFlattrThing; } - protected void onFlattred() { - String notificationTitle = context.getString(R.string.flattrd_label); - String notificationText = "", notificationSubText = "", notificationBigText = ""; - - // text for successfully flattred items - if (flattrd.size() == 1) - notificationText = String.format(context.getString(R.string.flattr_click_success)); - else if (flattrd.size() > 1) // flattred pending items from queue - notificationText = String.format(context.getString(R.string.flattr_click_success_count, flattrd.size())); - if (flattrd.size() > 0) { - String acc = ""; - for (String s : flattrd) - acc += s + '\n'; - acc = acc.substring(0, acc.length() - 2); + @Override + protected ExitCode doInBackground(Void... params) { - notificationBigText = String.format(context.getString(R.string.flattr_click_success_queue), acc); + if (!FlattrUtils.hasToken()) { + return ExitCode.NO_TOKEN; } - // add text for failures - if (flattr_failed.size() > 0) { - notificationTitle = context.getString(R.string.flattrd_failed_label); - notificationText = String.format(context.getString(R.string.flattr_click_failure_count), flattr_failed.size()) - + " " + notificationText; - - notificationSubText = flattr_failed.get(0); + if (!NetworkUtils.networkAvailable(context)) { + return ExitCode.NO_NETWORK; + } - String acc = ""; - for (String s : flattr_failed) - acc += s + '\n'; - acc = acc.substring(0, acc.length() - 2); + final List<FlattrThing> flattrQueue = DBReader.getFlattrQueue(context); + if (extraFlattrThing != null) { + flattrQueue.add(extraFlattrThing); + } else if (flattrQueue.size() == 1) { + // if only one item is flattrd, the report can specifically mentioned that this item has failed + extraFlattrThing = flattrQueue.get(0); + } - notificationBigText = String.format(context.getString(R.string.flattr_click_failure), acc) - + "\n" + notificationBigText; + if (flattrQueue.isEmpty()) { + return ExitCode.NO_THINGS; } - Log.d(TAG, "Going to post notification: " + notificationBigText); - - notificationManager.cancel(NOTIFICATION_ID); - - if (run_mode == FLATTR_NOTIFICATION || flattr_failed.size() > 0) { - PendingIntent contentIntent = PendingIntent.getActivity(context, 0, new Intent(context, MainActivity.class), 0); - if (android.os.Build.VERSION.SDK_INT >= 16) { - notificationBuilder = new Notification.BigTextStyle( - new Notification.Builder(context) - .setOngoing(false) - .setContentTitle(notificationTitle) - .setContentText(notificationText) - .setContentIntent(contentIntent) - .setSubText(notificationSubText) - .setSmallIcon(R.drawable.stat_notify_sync)) - .bigText(notificationText + "\n" + notificationBigText); - notificationManager.notify(NOTIFICATION_ID, notificationBuilder.build()); - } else { - notificationCompatBuilder = new NotificationCompat.Builder(context) // need new notificationBuilder and cancel/renotify to get rid of progress bar - .setContentTitle(notificationTitle) - .setContentText(notificationText) - .setContentIntent(contentIntent) - .setSubText(notificationBigText) - .setTicker(notificationTitle) - .setSmallIcon(R.drawable.stat_notify_sync) - .setOngoing(false); - notificationManager.notify(NOTIFICATION_ID, notificationCompatBuilder.build()); + List<Future> dbFutures = new LinkedList<Future>(); + for (FlattrThing thing : flattrQueue) { + if (BuildConfig.DEBUG) Log.d(TAG, "Processing " + thing.getTitle()); + + try { + thing.getFlattrStatus().setUnflattred(); // pop from queue to prevent unflattrable things from getting stuck in flattr queue infinitely + FlattrUtils.clickUrl(context, thing.getPaymentLink()); + thing.getFlattrStatus().setFlattred(); + publishProgress(R.string.flattr_click_success); + countSuccess++; + + } catch (FlattrException e) { + e.printStackTrace(); + countFailed++; + if (countFailed == 1) { + exception = e; + } } - } else if (run_mode == FLATTR_TOAST) { - Toast.makeText(context.getApplicationContext(), - notificationText, - Toast.LENGTH_LONG) - .show(); - } - } - protected void onEnqueue() { - Toast.makeText(context.getApplicationContext(), - R.string.flattr_click_enqueued, - Toast.LENGTH_LONG) - .show(); - } + Future<?> f = DBWriter.setFlattredStatus(context, thing, false); + if (f != null) { + dbFutures.add(f); + } + } - protected void onSetupNotification() { - if (android.os.Build.VERSION.SDK_INT >= 16) { - notificationBuilder = new Notification.BigTextStyle( - new Notification.Builder(context) - .setContentTitle(context.getString(R.string.flattring_label)) - .setAutoCancel(true) - .setSmallIcon(R.drawable.stat_notify_sync) - .setProgress(0, 0, true) - .setOngoing(true)); - } else { - notificationCompatBuilder = new NotificationCompat.Builder(context) - .setContentTitle(context.getString(R.string.flattring_label)) - .setAutoCancel(true) - .setSmallIcon(R.drawable.stat_notify_sync) - .setProgress(0, 0, true) - .setOngoing(true); + for (Future f : dbFutures) { + try { + f.get(); + } catch (InterruptedException e) { + e.printStackTrace(); + } catch (ExecutionException e) { + e.printStackTrace(); + } } - notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); + return ExitCode.EXIT_NORMAL; } @Override - protected void onPostExecute(Void result) { - if (BuildConfig.DEBUG) Log.d(TAG, "Exit code was " + exitCode); - + protected void onPostExecute(ExitCode exitCode) { + super.onPostExecute(exitCode); switch (exitCode) { - case NO_TOKEN: - notificationManager.cancel(NOTIFICATION_ID); - onNoAccessToken(); + case EXIT_NORMAL: + if (countFailed > 0) { + postFlattrFailedNotification(); + } break; - case ENQUEUED: - onEnqueue(); + case NO_NETWORK: + postToastNotification(R.string.flattr_click_enqueued); break; - case EXIT_DEFAULT: - onFlattred(); + case NO_TOKEN: + postNoTokenNotification(); break; - case NO_THINGS: // FlattrClickWorker called automatically somewhere to empty flattr queue - notificationManager.cancel(NOTIFICATION_ID); + case NO_THINGS: // nothing to notify here break; } } @Override - protected void onPreExecute() { - onSetupNotification(); + protected void onProgressUpdate(Integer... values) { + super.onProgressUpdate(values); + postToastNotification(values[0]); } - private static boolean haveInternetAccess(Context context) { - ConnectivityManager cm = - (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); - - NetworkInfo networkInfo = cm.getActiveNetworkInfo(); - return (networkInfo != null && networkInfo.isConnectedOrConnecting()); + private void postToastNotification(int msg) { + Toast.makeText(context, context.getString(msg), Toast.LENGTH_LONG).show(); } - @Override - protected Void doInBackground(Void... params) { - if (BuildConfig.DEBUG) Log.d(TAG, "Starting background work"); - - exitCode = EXIT_DEFAULT; - - if (!FlattrUtils.hasToken()) { - exitCode = NO_TOKEN; - } else if (DBReader.getFlattrQueueEmpty(context) && extra_flattr_thing == null) { - exitCode = NO_THINGS; - } else if (!haveInternetAccess(context) || run_mode == ENQUEUE_ONLY) { - exitCode = ENQUEUED; - } else { - List<FlattrThing> flattrList = DBReader.getFlattrQueue(context); - Log.d(TAG, "flattrQueue processing list with " + flattrList.size() + " items."); - - if (extra_flattr_thing != null) - flattrList.add(extra_flattr_thing); - - flattrd.ensureCapacity(flattrList.size()); - - for (FlattrThing thing : flattrList) { - try { - Log.d(TAG, "flattrQueue processing " + thing.getTitle() + " " + thing.getPaymentLink()); - publishProgress(String.format(context.getString(R.string.flattring_thing), thing.getTitle())); - - thing.getFlattrStatus().setUnflattred(); // pop from queue to prevent unflattrable things from getting stuck in flattr queue infinitely - - FlattrUtils.clickUrl(context, thing.getPaymentLink()); - flattrd.add(thing.getTitle()); - - thing.getFlattrStatus().setFlattred(); - } catch (Exception e) { - Log.d(TAG, "flattrQueue processing exception at item " + thing.getTitle() + " " + e.getMessage()); - flattr_failed.ensureCapacity(flattrList.size()); - flattr_failed.add(thing.getTitle() + ": " + e.getMessage()); - } - Log.d(TAG, "flattrQueue processing - going to write thing back to db with flattr_status " + Long.toString(thing.getFlattrStatus().toLong())); - DBWriter.setFlattredStatus(context, thing, false); - } + private void postNoTokenNotification() { + PendingIntent contentIntent = PendingIntent.getActivity(context, 0, new Intent(context, FlattrAuthActivity.class), 0); + + Notification notification = new NotificationCompat.Builder(context) + .setStyle(new NotificationCompat.BigTextStyle().bigText(context.getString(R.string.no_flattr_token_notification_msg))) + .setContentIntent(contentIntent) + .setContentTitle(context.getString(R.string.no_flattr_token_title)) + .setTicker(context.getString(R.string.no_flattr_token_title)) + .setSmallIcon(R.drawable.stat_notify_sync_error) + .setOngoing(false) + .setAutoCancel(true) + .build(); + ((NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE)).notify(NOTIFICATION_ID, notification); + } + private void postFlattrFailedNotification() { + if (countFailed == 0) { + return; } - return null; - } - - @Override - protected void onProgressUpdate(String... names) { PendingIntent contentIntent = PendingIntent.getActivity(context, 0, new Intent(context, MainActivity.class), 0); - if (android.os.Build.VERSION.SDK_INT >= 16) { - notificationBuilder.setBigContentTitle(names[0]); - notificationManager.notify(NOTIFICATION_ID, notificationBuilder.build()); + String title; + String subtext; + + if (countFailed == 1) { + title = context.getString(R.string.flattrd_failed_label); + String exceptionMsg = (exception.getMessage() != null) ? exception.getMessage() : ""; + subtext = context.getString(R.string.flattr_click_failure, extraFlattrThing.getTitle()) + + "\n" + exceptionMsg; } else { - notificationCompatBuilder.setContentText(names[0]); - notificationCompatBuilder.setContentIntent(contentIntent); - notificationManager.notify(NOTIFICATION_ID, notificationCompatBuilder.build()); + title = context.getString(R.string.flattrd_label); + subtext = context.getString(R.string.flattr_click_success_count, countSuccess) + "\n" + + context.getString(R.string.flattr_click_failure_count, countFailed); } + + Notification notification = new NotificationCompat.Builder(context) + .setStyle(new NotificationCompat.BigTextStyle().bigText(subtext)) + .setContentIntent(contentIntent) + .setContentTitle(title) + .setTicker(title) + .setSmallIcon(R.drawable.stat_notify_sync_error) + .setOngoing(false) + .setAutoCancel(true) + .build(); + ((NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE)).notify(NOTIFICATION_ID, notification); } - @SuppressLint("NewApi") + + /** + * Starts the FlattrClickWorker as an AsyncTask. + */ + @TargetApi(11) public void executeAsync() { - FlattrUtils.hasToken(); if (android.os.Build.VERSION.SDK_INT > android.os.Build.VERSION_CODES.GINGERBREAD_MR1) { - executeOnExecutor(THREAD_POOL_EXECUTOR); + executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); } else { execute(); } diff --git a/src/de/danoeh/antennapod/asynctask/ImageDiskCache.java b/src/de/danoeh/antennapod/asynctask/ImageDiskCache.java index 1d069daa5..77609f28b 100644 --- a/src/de/danoeh/antennapod/asynctask/ImageDiskCache.java +++ b/src/de/danoeh/antennapod/asynctask/ImageDiskCache.java @@ -11,6 +11,7 @@ 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 org.apache.commons.lang3.Validate; import java.io.*; import java.util.ArrayList; @@ -48,9 +49,8 @@ public class ImageDiskCache { * 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(); - } + Validate.notNull(path); + if (cacheSingletons.containsKey(path)) { return cacheSingletons.get(path); } @@ -358,9 +358,7 @@ public class ImageDiskCache { private final long size; public DiskCacheObject(String fileUrl, long size) { - if (fileUrl == null) { - throw new NullPointerException(); - } + Validate.notNull(fileUrl); this.fileUrl = fileUrl; this.timestamp = System.currentTimeMillis(); this.size = size; diff --git a/src/de/danoeh/antennapod/dialog/AutoFlattrPreferenceDialog.java b/src/de/danoeh/antennapod/dialog/AutoFlattrPreferenceDialog.java new file mode 100644 index 000000000..d1ed795dc --- /dev/null +++ b/src/de/danoeh/antennapod/dialog/AutoFlattrPreferenceDialog.java @@ -0,0 +1,107 @@ +package de.danoeh.antennapod.dialog; + +import android.annotation.SuppressLint; +import android.app.Activity; +import android.app.AlertDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.view.View; +import android.widget.CheckBox; +import android.widget.SeekBar; +import android.widget.TextView; + +import org.apache.commons.lang3.Validate; + +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.preferences.UserPreferences; + +/** + * Creates a new AlertDialog that displays preferences for auto-flattring to the user. + */ +public class AutoFlattrPreferenceDialog { + + private AutoFlattrPreferenceDialog() { + } + + public static void newAutoFlattrPreferenceDialog(final Activity activity, final AutoFlattrPreferenceDialogInterface callback) { + Validate.notNull(activity); + Validate.notNull(callback); + + AlertDialog.Builder builder = new AlertDialog.Builder(activity); + + @SuppressLint("InflateParams") View view = activity.getLayoutInflater().inflate(R.layout.autoflattr_preference_dialog, null); + final CheckBox chkAutoFlattr = (CheckBox) view.findViewById(R.id.chkAutoFlattr); + final SeekBar skbPercent = (SeekBar) view.findViewById(R.id.skbPercent); + final TextView txtvStatus = (TextView) view.findViewById(R.id.txtvStatus); + + chkAutoFlattr.setChecked(UserPreferences.isAutoFlattr()); + skbPercent.setEnabled(chkAutoFlattr.isChecked()); + txtvStatus.setEnabled(chkAutoFlattr.isChecked()); + + final int initialValue = (int) (UserPreferences.getAutoFlattrPlayedDurationThreshold() * 100.0f); + setStatusMsgText(activity, txtvStatus, initialValue); + skbPercent.setProgress(initialValue); + + chkAutoFlattr.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + skbPercent.setEnabled(chkAutoFlattr.isChecked()); + txtvStatus.setEnabled(chkAutoFlattr.isChecked()); + } + }); + + skbPercent.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { + @Override + public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { + setStatusMsgText(activity, txtvStatus, progress); + } + + @Override + public void onStartTrackingTouch(SeekBar seekBar) { + + } + + @Override + public void onStopTrackingTouch(SeekBar seekBar) { + + } + }); + + builder.setTitle(R.string.pref_auto_flattr_title) + .setView(view) + .setPositiveButton(R.string.confirm_label, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + float progDouble = ((float) skbPercent.getProgress()) / 100.0f; + callback.onConfirmed(chkAutoFlattr.isChecked(), progDouble); + dialog.dismiss(); + } + }) + .setNegativeButton(R.string.cancel_label, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + callback.onCancelled(); + dialog.dismiss(); + } + }) + .setCancelable(false).show(); + } + + private static void setStatusMsgText(Context context, TextView txtvStatus, int progress) { + if (progress == 0) { + txtvStatus.setText(R.string.auto_flattr_ater_beginning); + } else if (progress == 100) { + txtvStatus.setText(R.string.auto_flattr_ater_end); + } else { + txtvStatus.setText(context.getString(R.string.auto_flattr_after_percent, progress)); + } + } + + public static interface AutoFlattrPreferenceDialogInterface { + public void onCancelled(); + + public void onConfirmed(boolean autoFlattrEnabled, float autoFlattrValue); + } + + +} diff --git a/src/de/danoeh/antennapod/dialog/FeedItemDialog.java b/src/de/danoeh/antennapod/dialog/FeedItemDialog.java index 4a130af6e..7384463de 100644 --- a/src/de/danoeh/antennapod/dialog/FeedItemDialog.java +++ b/src/de/danoeh/antennapod/dialog/FeedItemDialog.java @@ -1,5 +1,6 @@ package de.danoeh.antennapod.dialog; +import android.annotation.TargetApi; import android.app.Dialog; import android.content.ActivityNotFoundException; import android.content.Context; @@ -21,6 +22,14 @@ import android.webkit.WebViewClient; import android.widget.ImageButton; import android.widget.TextView; import android.widget.Toast; + +import org.apache.commons.lang3.StringEscapeUtils; +import org.apache.commons.lang3.Validate; + +import java.util.Collection; +import java.util.List; +import java.util.concurrent.Callable; + import de.danoeh.antennapod.BuildConfig; import de.danoeh.antennapod.R; import de.danoeh.antennapod.adapter.DefaultActionButtonCallback; @@ -34,11 +43,6 @@ import de.danoeh.antennapod.storage.DownloadRequester; import de.danoeh.antennapod.util.QueueAccess; import de.danoeh.antennapod.util.ShownotesProvider; import de.danoeh.antennapod.util.menuhandler.FeedItemMenuHandler; -import org.apache.commons.lang3.StringEscapeUtils; - -import java.util.Collection; -import java.util.List; -import java.util.concurrent.Callable; /** * Shows information about a specific FeedItem and provides actions like playing, downloading, etc. @@ -58,7 +62,7 @@ public class FeedItemDialog extends Dialog { private PopupMenu popupMenu; public static FeedItemDialog newInstance(Context context, FeedItemDialogSavedInstance savedInstance) { - if (savedInstance == null) throw new IllegalArgumentException("savedInstance = null"); + Validate.notNull(savedInstance); FeedItemDialog dialog = newInstance(context, savedInstance.item, savedInstance.queueAccess); if (savedInstance.isShowing) { dialog.show(); @@ -76,8 +80,8 @@ public class FeedItemDialog extends Dialog { public FeedItemDialog(Context context, int theme, FeedItem item, QueueAccess queue) { super(context, theme); - if (item == null) throw new IllegalArgumentException("item = null"); - if (queue == null) throw new IllegalArgumentException("queue = null"); + Validate.notNull(item); + Validate.notNull(queue); this.item = item; this.queue = queue; } @@ -95,6 +99,7 @@ public class FeedItemDialog extends Dialog { && UserPreferences.getTheme() != R.style.Theme_AntennaPod_Dark; } + @TargetApi(Build.VERSION_CODES.HONEYCOMB) @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -147,12 +152,9 @@ public class FeedItemDialog extends Dialog { @Override public void onClick(View v) { - FeedMedia media = item.getMedia(); - if (media == null) { - return; - } actionButtonCallback.onActionButtonPressed(item); - if (media.isDownloaded()) { + FeedMedia media = item.getMedia(); + if (media != null && media.isDownloaded()) { // playback was started, dialog should close itself dismiss(); } @@ -166,16 +168,17 @@ public class FeedItemDialog extends Dialog { { @Override public void onClick(View v) { - FeedMedia media = item.getMedia(); - if (media == null) { - return; - } - - if (!media.isDownloaded()) { - DBTasks.playMedia(getContext(), media, true, true, true); - dismiss(); - } else { - DBWriter.deleteFeedMediaOfItem(getContext(), media.getId()); + if (item.hasMedia()) { + FeedMedia media = item.getMedia(); + if (!media.isDownloaded()) { + DBTasks.playMedia(getContext(), media, true, true, true); + dismiss(); + } else { + DBWriter.deleteFeedMediaOfItem(getContext(), media.getId()); + } + } else if (item.getLink() != null) { + Uri uri = Uri.parse(item.getLink()); + getContext().startActivity(new Intent(Intent.ACTION_VIEW, uri)); } } } @@ -186,7 +189,13 @@ public class FeedItemDialog extends Dialog { public void onClick(View v) { popupMenu.getMenu().clear(); popupMenu.inflate(R.menu.feeditem_dialog); - FeedItemMenuHandler.onPrepareMenu(popupMenuInterface, item, true, queue); + if (item.hasMedia()) { + FeedItemMenuHandler.onPrepareMenu(popupMenuInterface, item, true, queue); + } else { + // these are already available via button1 and button2 + FeedItemMenuHandler.onPrepareMenu(popupMenuInterface, item, true, queue, + R.id.mark_read_item, R.id.visit_website_item); + } popupMenu.show(); } } @@ -228,9 +237,26 @@ public class FeedItemDialog extends Dialog { } FeedMedia media = item.getMedia(); if (media == null) { - header.setVisibility(View.GONE); + TypedArray drawables = getContext().obtainStyledAttributes(new int[]{R.attr.navigation_accept, + R.attr.location_web_site}); + + if (!item.isRead()) { + butAction1.setImageDrawable(drawables.getDrawable(0)); + butAction1.setContentDescription(getContext().getString(R.string.mark_read_label)); + butAction1.setVisibility(View.VISIBLE); + } else { + butAction1.setVisibility(View.INVISIBLE); + } + + if (item.getLink() != null) { + butAction2.setImageDrawable(drawables.getDrawable(1)); + butAction2.setContentDescription(getContext().getString(R.string.visit_website_label)); + } else { + butAction2.setEnabled(false); + } + + drawables.recycle(); } else { - header.setVisibility(View.VISIBLE); boolean isDownloading = DownloadRequester.getInstance().isDownloadingFile(media); TypedArray drawables = getContext().obtainStyledAttributes(new int[]{R.attr.av_play, R.attr.av_download, R.attr.action_stream, R.attr.content_discard, R.attr.navigation_cancel}); @@ -348,7 +374,7 @@ public class FeedItemDialog extends Dialog { public void setItem(FeedItem item) { - if (item == null) throw new IllegalArgumentException("item = null"); + Validate.notNull(item); this.item = item; } @@ -369,7 +395,7 @@ public class FeedItemDialog extends Dialog { } public void setQueue(QueueAccess queue) { - if (queue == null) throw new IllegalArgumentException("queue = null"); + Validate.notNull(queue); this.queue = queue; } @@ -387,7 +413,7 @@ public class FeedItemDialog extends Dialog { /** * Used to save the FeedItemDialog's state across configuration changes - * */ + */ public static class FeedItemDialogSavedInstance { final FeedItem item; final QueueAccess queueAccess; diff --git a/src/de/danoeh/antennapod/dialog/TimeDialog.java b/src/de/danoeh/antennapod/dialog/TimeDialog.java index cb3ebf0ab..bbd514640 100644 --- a/src/de/danoeh/antennapod/dialog/TimeDialog.java +++ b/src/de/danoeh/antennapod/dialog/TimeDialog.java @@ -8,6 +8,7 @@ import android.text.TextWatcher; import android.util.Log; import android.view.View; import android.view.Window; +import android.view.inputmethod.InputMethodManager; import android.widget.*; import de.danoeh.antennapod.BuildConfig; import de.danoeh.antennapod.R; @@ -15,114 +16,123 @@ import de.danoeh.antennapod.R; import java.util.concurrent.TimeUnit; public abstract class TimeDialog extends Dialog { - private static final String TAG = "TimeDialog"; + private static final String TAG = "TimeDialog"; - private static final int DEFAULT_SPINNER_POSITION = 1; + private static final int DEFAULT_SPINNER_POSITION = 1; - private Context context; + private Context context; - private EditText etxtTime; - private Spinner spTimeUnit; - private Button butConfirm; - private Button butCancel; + private EditText etxtTime; + private Spinner spTimeUnit; + private Button butConfirm; + private Button butCancel; - private TimeUnit[] units = { TimeUnit.SECONDS, TimeUnit.MINUTES, - TimeUnit.HOURS }; + private TimeUnit[] units = {TimeUnit.SECONDS, TimeUnit.MINUTES, + TimeUnit.HOURS}; - public TimeDialog(Context context, int titleTextId, int leftButtonTextId) { - super(context); - this.context = context; - } + public TimeDialog(Context context, int titleTextId, int leftButtonTextId) { + super(context); + this.context = context; + } - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - requestWindowFeature(Window.FEATURE_NO_TITLE); + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + requestWindowFeature(Window.FEATURE_NO_TITLE); String[] spinnerContent = new String[]{context.getString(R.string.time_unit_seconds), - context.getString(R.string.time_unit_minutes), - context.getString(R.string.time_unit_hours)}; - - setContentView(R.layout.time_dialog); - etxtTime = (EditText) findViewById(R.id.etxtTime); - spTimeUnit = (Spinner) findViewById(R.id.spTimeUnit); - butConfirm = (Button) findViewById(R.id.butConfirm); - butCancel = (Button) findViewById(R.id.butCancel); - - butConfirm.setText(R.string.set_sleeptimer_label); - butCancel.setText(R.string.cancel_label); - setTitle(R.string.set_sleeptimer_label); - ArrayAdapter<String> spinnerAdapter = new ArrayAdapter<String>( - this.getContext(), android.R.layout.simple_spinner_item, - spinnerContent); - spinnerAdapter - .setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); - spTimeUnit.setAdapter(spinnerAdapter); - spTimeUnit.setSelection(DEFAULT_SPINNER_POSITION); - butCancel.setOnClickListener(new View.OnClickListener() { - - @Override - public void onClick(View v) { - dismiss(); - } - }); - butConfirm.setOnClickListener(new View.OnClickListener() { - - @Override - public void onClick(View v) { - try { - long input = readTimeMillis(); - onTimeEntered(input); - dismiss(); - } catch (NumberFormatException e) { - e.printStackTrace(); - Toast toast = Toast.makeText(context, - R.string.time_dialog_invalid_input, - Toast.LENGTH_LONG); - toast.show(); - } - } - }); - etxtTime.addTextChangedListener(new TextWatcher() { - - @Override - public void afterTextChanged(Editable s) { - checkInputLength(s.length()); - } - - @Override - public void beforeTextChanged(CharSequence s, int start, int count, - int after) { - - } - - @Override - public void onTextChanged(CharSequence s, int start, int before, - int count) { - - } - }); - checkInputLength(etxtTime.getText().length()); - - } - - private void checkInputLength(int length) { - if (length > 0) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Length is larger than 0, enabling confirm button"); - butConfirm.setEnabled(true); - } else { - if (BuildConfig.DEBUG) - Log.d(TAG, "Length is smaller than 0, disabling confirm button"); - butConfirm.setEnabled(false); - } - } - - public abstract void onTimeEntered(long millis); - - private long readTimeMillis() { - TimeUnit selectedUnit = units[spTimeUnit.getSelectedItemPosition()]; - long value = Long.valueOf(etxtTime.getText().toString()); - return selectedUnit.toMillis(value); - } + context.getString(R.string.time_unit_minutes), + context.getString(R.string.time_unit_hours)}; + + setContentView(R.layout.time_dialog); + etxtTime = (EditText) findViewById(R.id.etxtTime); + spTimeUnit = (Spinner) findViewById(R.id.spTimeUnit); + butConfirm = (Button) findViewById(R.id.butConfirm); + butCancel = (Button) findViewById(R.id.butCancel); + + butConfirm.setText(R.string.set_sleeptimer_label); + butCancel.setText(R.string.cancel_label); + setTitle(R.string.set_sleeptimer_label); + ArrayAdapter<String> spinnerAdapter = new ArrayAdapter<String>( + this.getContext(), android.R.layout.simple_spinner_item, + spinnerContent); + spinnerAdapter + .setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + spTimeUnit.setAdapter(spinnerAdapter); + spTimeUnit.setSelection(DEFAULT_SPINNER_POSITION); + butCancel.setOnClickListener(new View.OnClickListener() { + + @Override + public void onClick(View v) { + dismiss(); + } + }); + butConfirm.setOnClickListener(new View.OnClickListener() { + + @Override + public void onClick(View v) { + try { + long input = readTimeMillis(); + onTimeEntered(input); + dismiss(); + } catch (NumberFormatException e) { + e.printStackTrace(); + Toast toast = Toast.makeText(context, + R.string.time_dialog_invalid_input, + Toast.LENGTH_LONG); + toast.show(); + } + } + }); + etxtTime.addTextChangedListener(new TextWatcher() { + + @Override + public void afterTextChanged(Editable s) { + checkInputLength(s.length()); + } + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, + int after) { + + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, + int count) { + + } + }); + checkInputLength(etxtTime.getText().length()); + etxtTime.postDelayed(new Runnable() { + @Override + public void run() { + InputMethodManager imm = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE); + imm.showSoftInput(etxtTime, InputMethodManager.SHOW_IMPLICIT); + } + }, 100); + + + + } + + private void checkInputLength(int length) { + if (length > 0) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Length is larger than 0, enabling confirm button"); + butConfirm.setEnabled(true); + } else { + if (BuildConfig.DEBUG) + Log.d(TAG, "Length is smaller than 0, disabling confirm button"); + butConfirm.setEnabled(false); + } + } + + public abstract void onTimeEntered(long millis); + + private long readTimeMillis() { + TimeUnit selectedUnit = units[spTimeUnit.getSelectedItemPosition()]; + long value = Long.valueOf(etxtTime.getText().toString()); + return selectedUnit.toMillis(value); + } } diff --git a/src/de/danoeh/antennapod/feed/EventDistributor.java b/src/de/danoeh/antennapod/feed/EventDistributor.java index 85a9b5727..5fb72048e 100644 --- a/src/de/danoeh/antennapod/feed/EventDistributor.java +++ b/src/de/danoeh/antennapod/feed/EventDistributor.java @@ -2,6 +2,9 @@ package de.danoeh.antennapod.feed; import android.os.Handler; import android.util.Log; + +import org.apache.commons.lang3.Validate; + import de.danoeh.antennapod.BuildConfig; import java.util.AbstractQueue; @@ -90,10 +93,7 @@ public class EventDistributor extends Observable { @Override public void addObserver(Observer observer) { super.addObserver(observer); - if (!(observer instanceof EventListener)) { - throw new IllegalArgumentException( - "Observer must be instance of EventListener"); - } + Validate.isInstanceOf(EventListener.class, observer); } public void sendDownloadQueuedBroadcast() { diff --git a/src/de/danoeh/antennapod/feed/FeedMedia.java b/src/de/danoeh/antennapod/feed/FeedMedia.java index 1f8e7f8f8..dc941cb48 100644 --- a/src/de/danoeh/antennapod/feed/FeedMedia.java +++ b/src/de/danoeh/antennapod/feed/FeedMedia.java @@ -401,7 +401,9 @@ public class FeedMedia extends FeedFile implements Playable { @Override public String getImageLoaderCacheKey() { String out; - if (item.hasItemImageDownloaded()) { + if (item == null) { + return null; + } else if (item.hasItemImageDownloaded()) { out = item.getImageLoaderCacheKey(); } else { out = new Playable.DefaultPlayableImageLoader(this) diff --git a/src/de/danoeh/antennapod/fragment/CompletedDownloadsFragment.java b/src/de/danoeh/antennapod/fragment/CompletedDownloadsFragment.java index ed304ad37..082fe93fc 100644 --- a/src/de/danoeh/antennapod/fragment/CompletedDownloadsFragment.java +++ b/src/de/danoeh/antennapod/fragment/CompletedDownloadsFragment.java @@ -68,6 +68,7 @@ public class CompletedDownloadsFragment extends ListFragment { listAdapter = null; viewCreated = false; feedItemDialog = null; + stopItemLoader(); } @Override @@ -103,6 +104,7 @@ public class CompletedDownloadsFragment extends ListFragment { listAdapter = new DownloadedEpisodesListAdapter(getActivity(), itemAccess); setListAdapter(listAdapter); } + setListShown(true); listAdapter.notifyDataSetChanged(); if (feedItemDialog != null) { boolean res = feedItemDialog.updateContent(queue, items); @@ -171,7 +173,6 @@ public class CompletedDownloadsFragment extends ListFragment { @Override protected void onPostExecute(Object[] results) { super.onPostExecute(results); - setListShown(true); if (results != null) { items = (List<FeedItem>) results[0]; queue = (QueueAccess) results[1]; diff --git a/src/de/danoeh/antennapod/fragment/ItemDescriptionFragment.java b/src/de/danoeh/antennapod/fragment/ItemDescriptionFragment.java index bf6974982..04c7fbf8e 100644 --- a/src/de/danoeh/antennapod/fragment/ItemDescriptionFragment.java +++ b/src/de/danoeh/antennapod/fragment/ItemDescriptionFragment.java @@ -2,8 +2,11 @@ package de.danoeh.antennapod.fragment; import android.annotation.SuppressLint; import android.app.Activity; -import android.content.*; -import android.content.res.TypedArray; +import android.content.ActivityNotFoundException; +import android.content.ClipData; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; import android.net.Uri; import android.os.AsyncTask; import android.os.Build; @@ -11,26 +14,33 @@ import android.os.Bundle; import android.support.v4.app.Fragment; import android.support.v7.app.ActionBarActivity; import android.util.Log; -import android.util.TypedValue; -import android.view.*; +import android.view.ContextMenu; import android.view.ContextMenu.ContextMenuInfo; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; import android.webkit.WebSettings.LayoutAlgorithm; import android.webkit.WebView; import android.webkit.WebViewClient; import android.widget.Toast; + import de.danoeh.antennapod.BuildConfig; import de.danoeh.antennapod.R; import de.danoeh.antennapod.feed.FeedItem; import de.danoeh.antennapod.preferences.UserPreferences; import de.danoeh.antennapod.storage.DBReader; +import de.danoeh.antennapod.util.Converter; import de.danoeh.antennapod.util.ShareUtils; import de.danoeh.antennapod.util.ShownotesProvider; import de.danoeh.antennapod.util.playback.Playable; -import org.apache.commons.lang3.StringEscapeUtils; - -import java.util.concurrent.Callable; +import de.danoeh.antennapod.util.playback.PlaybackController; +import de.danoeh.antennapod.util.playback.Timeline; -/** Displays the description of a Playable object in a Webview. */ +/** + * Displays the description of a Playable object in a Webview. + */ public class ItemDescriptionFragment extends Fragment { private static final String TAG = "ItemDescriptionFragment"; @@ -43,6 +53,7 @@ public class ItemDescriptionFragment extends Fragment { private static final String ARG_FEEDITEM_ID = "arg.feeditem"; private static final String ARG_SAVE_STATE = "arg.saveState"; + private static final String ARG_HIGHLIGHT_TIMECODES = "arg.highlightTimecodes"; private WebView webvDescription; @@ -63,21 +74,29 @@ public class ItemDescriptionFragment extends Fragment { */ private boolean saveState; + /** + * True if Fragment should highlight timecodes (e.g. time codes in the HH:MM:SS format). + */ + private boolean highlightTimecodes; + public static ItemDescriptionFragment newInstance(Playable media, - boolean saveState) { + boolean saveState, + boolean highlightTimecodes) { ItemDescriptionFragment f = new ItemDescriptionFragment(); Bundle args = new Bundle(); args.putParcelable(ARG_PLAYABLE, media); args.putBoolean(ARG_SAVE_STATE, saveState); + args.putBoolean(ARG_HIGHLIGHT_TIMECODES, highlightTimecodes); f.setArguments(args); return f; } - public static ItemDescriptionFragment newInstance(FeedItem item, boolean saveState) { + public static ItemDescriptionFragment newInstance(FeedItem item, boolean saveState, boolean highlightTimecodes) { ItemDescriptionFragment f = new ItemDescriptionFragment(); Bundle args = new Bundle(); args.putLong(ARG_FEEDITEM_ID, item.getId()); args.putBoolean(ARG_SAVE_STATE, saveState); + args.putBoolean(ARG_HIGHLIGHT_TIMECODES, highlightTimecodes); f.setArguments(args); return f; } @@ -106,12 +125,16 @@ public class ItemDescriptionFragment extends Fragment { @Override public boolean shouldOverrideUrlLoading(WebView view, String url) { - Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); - try { - startActivity(intent); - } catch (ActivityNotFoundException e) { - e.printStackTrace(); - return false; + if (Timeline.isTimecodeLink(url)) { + onTimecodeLinkSelected(url); + } else { + Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); + try { + startActivity(intent); + } catch (ActivityNotFoundException e) { + e.printStackTrace(); + return true; + } } return true; } @@ -178,6 +201,7 @@ public class ItemDescriptionFragment extends Fragment { Log.d(TAG, "Creating fragment"); Bundle args = getArguments(); saveState = args.getBoolean(ARG_SAVE_STATE, false); + highlightTimecodes = args.getBoolean(ARG_HIGHLIGHT_TIMECODES, false); } @@ -229,21 +253,6 @@ public class ItemDescriptionFragment extends Fragment { } } - /** - * Return the CSS style of the Webview. - * - * @param textColor the default color to use for the text in the webview. This - * value is inserted directly into the CSS String. - */ - private String applyWebviewStyle(String textColor, String data) { - final String WEBVIEW_STYLE = "<html><head><style type=\"text/css\"> @font-face { font-family: 'Roboto-Light'; src: url('file:///android_asset/Roboto-Light.ttf'); } * { color: %s; font-family: roboto-Light; font-size: 11pt; } a { font-style: normal; text-decoration: none; font-weight: normal; color: #00A8DF; } img { display: block; margin: 10 auto; max-width: %s; height: auto; } body { margin: %dpx %dpx %dpx %dpx; }</style></head><body>%s</body></html>"; - final int pageMargin = (int) TypedValue.applyDimension( - TypedValue.COMPLEX_UNIT_DIP, 8, getResources() - .getDisplayMetrics()); - return String.format(WEBVIEW_STYLE, textColor, "100%", pageMargin, - pageMargin, pageMargin, pageMargin, data); - } - private View.OnLongClickListener webViewLongClickListener = new View.OnLongClickListener() { @Override @@ -254,7 +263,8 @@ public class ItemDescriptionFragment extends Fragment { if (BuildConfig.DEBUG) Log.d(TAG, "Link of webview was long-pressed. Extra: " - + r.getExtra()); + + r.getExtra() + ); selectedURL = r.getExtra(); webvDescription.showContextMenu(); return true; @@ -295,6 +305,13 @@ public class ItemDescriptionFragment extends Fragment { R.string.copied_url_msg, Toast.LENGTH_SHORT); t.show(); break; + case R.id.go_to_position_item: + if (Timeline.isTimecodeLink(selectedURL)) { + onTimecodeLinkSelected(selectedURL); + } else { + Log.e(TAG, "Selected go_to_position_item, but URL was no timecode link: " + selectedURL); + } + break; default: handled = false; break; @@ -311,13 +328,19 @@ public class ItemDescriptionFragment extends Fragment { ContextMenuInfo menuInfo) { if (selectedURL != null) { super.onCreateContextMenu(menu, v, menuInfo); - menu.add(Menu.NONE, R.id.open_in_browser_item, Menu.NONE, - R.string.open_in_browser_label); - menu.add(Menu.NONE, R.id.copy_url_item, Menu.NONE, - R.string.copy_url_label); - menu.add(Menu.NONE, R.id.share_url_item, Menu.NONE, - R.string.share_url_label); - menu.setHeaderTitle(selectedURL); + if (Timeline.isTimecodeLink(selectedURL)) { + menu.add(Menu.NONE, R.id.go_to_position_item, Menu.NONE, + R.string.go_to_position_label); + menu.setHeaderTitle(Converter.getDurationStringLong(Timeline.getTimecodeLinkTime(selectedURL))); + } else { + menu.add(Menu.NONE, R.id.open_in_browser_item, Menu.NONE, + R.string.open_in_browser_label); + menu.add(Menu.NONE, R.id.copy_url_item, Menu.NONE, + R.string.copy_url_label); + menu.add(Menu.NONE, R.id.share_url_item, Menu.NONE, + R.string.share_url_label); + menu.setHeaderTitle(selectedURL); + } } } @@ -364,22 +387,10 @@ public class ItemDescriptionFragment extends Fragment { if (BuildConfig.DEBUG) Log.d(TAG, "Loading Webview"); try { - Callable<String> shownotesLoadTask = shownotesProvider.loadShownotes(); - final String shownotes = shownotesLoadTask.call(); - - data = StringEscapeUtils.unescapeHtml4(shownotes); Activity activity = getActivity(); if (activity != null) { - TypedArray res = activity - .getTheme() - .obtainStyledAttributes( - new int[]{android.R.attr.textColorPrimary}); - int colorResource = res.getColor(0, 0); - String colorString = String.format("#%06X", - 0xFFFFFF & colorResource); - Log.i(TAG, "text color: " + colorString); - res.recycle(); - data = applyWebviewStyle(colorString, data); + Timeline timeline = new Timeline(activity, shownotesProvider); + data = timeline.processShownotes(highlightTimecodes); } else { cancel(true); } @@ -409,7 +420,8 @@ public class ItemDescriptionFragment extends Fragment { if (BuildConfig.DEBUG) Log.d(TAG, "Saving scroll position: " - + webvDescription.getScrollY()); + + webvDescription.getScrollY() + ); editor.putInt(PREF_SCROLL_Y, webvDescription.getScrollY()); editor.putString(PREF_PLAYABLE_ID, media.getIdentifier() .toString()); @@ -447,4 +459,18 @@ public class ItemDescriptionFragment extends Fragment { } return false; } + + private void onTimecodeLinkSelected(String link) { + int time = Timeline.getTimecodeLinkTime(link); + if (getActivity() != null && getActivity() instanceof ItemDescriptionFragmentCallback) { + PlaybackController pc = ((ItemDescriptionFragmentCallback) getActivity()).getPlaybackController(); + if (pc != null) { + pc.seekTo(time); + } + } + } + + public interface ItemDescriptionFragmentCallback { + public PlaybackController getPlaybackController(); + } } diff --git a/src/de/danoeh/antennapod/fragment/ItemlistFragment.java b/src/de/danoeh/antennapod/fragment/ItemlistFragment.java index 5d0b1bec8..d37f17b6d 100644 --- a/src/de/danoeh/antennapod/fragment/ItemlistFragment.java +++ b/src/de/danoeh/antennapod/fragment/ItemlistFragment.java @@ -12,11 +12,20 @@ import android.support.v4.app.ListFragment; import android.support.v7.app.ActionBarActivity; import android.support.v7.widget.SearchView; import android.util.Log; -import android.view.*; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; import android.widget.ImageButton; import android.widget.ImageView; import android.widget.ListView; import android.widget.TextView; + +import org.apache.commons.lang3.Validate; + +import java.util.List; + import de.danoeh.antennapod.BuildConfig; import de.danoeh.antennapod.R; import de.danoeh.antennapod.activity.FeedInfoActivity; @@ -41,8 +50,7 @@ import de.danoeh.antennapod.storage.DownloadRequester; import de.danoeh.antennapod.util.QueueAccess; import de.danoeh.antennapod.util.menuhandler.FeedMenuHandler; import de.danoeh.antennapod.util.menuhandler.MenuItemUtils; - -import java.util.List; +import de.danoeh.antennapod.util.menuhandler.NavDrawerActivity; /** * Displays a list of FeedItems. @@ -97,7 +105,7 @@ public class ItemlistFragment extends ListFragment { setHasOptionsMenu(true); Bundle args = getArguments(); - if (args == null) throw new IllegalArgumentException("args invalid"); + Validate.notNull(args); feedID = args.getLong(ARGUMENT_FEED_ID); } @@ -155,31 +163,36 @@ public class ItemlistFragment extends ListFragment { @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { super.onCreateOptionsMenu(menu, inflater); - FeedMenuHandler.onCreateOptionsMenu(inflater, menu); - final SearchView sv = new SearchView(getActivity()); - MenuItemUtils.addSearchItem(menu, sv); - sv.setQueryHint(getString(R.string.search_hint)); - sv.setOnQueryTextListener(new SearchView.OnQueryTextListener() { - @Override - public boolean onQueryTextSubmit(String s) { - sv.clearFocus(); - if (itemsLoaded) { - ((MainActivity) getActivity()).loadChildFragment(SearchFragment.newInstance(s, feed.getId())); + if (itemsLoaded && !MenuItemUtils.isActivityDrawerOpen((NavDrawerActivity) getActivity())) { + FeedMenuHandler.onCreateOptionsMenu(inflater, menu); + + final SearchView sv = new SearchView(getActivity()); + MenuItemUtils.addSearchItem(menu, sv); + sv.setQueryHint(getString(R.string.search_hint)); + sv.setOnQueryTextListener(new SearchView.OnQueryTextListener() { + @Override + public boolean onQueryTextSubmit(String s) { + sv.clearFocus(); + if (itemsLoaded) { + ((MainActivity) getActivity()).loadChildFragment(SearchFragment.newInstance(s, feed.getId())); + } + return true; } - return true; - } - @Override - public boolean onQueryTextChange(String s) { - return false; - } - }); + @Override + public boolean onQueryTextChange(String s) { + return false; + } + }); + } } @Override public void onPrepareOptionsMenu(Menu menu) { - FeedMenuHandler.onPrepareOptionsMenu(menu, feed); + if (itemsLoaded && !MenuItemUtils.isActivityDrawerOpen((NavDrawerActivity) getActivity())) { + FeedMenuHandler.onPrepareOptionsMenu(menu, feed); + } } @Override @@ -322,10 +335,11 @@ public class ItemlistFragment extends ListFragment { Log.e(TAG, "Unable to setup listview: listView = null or feed = null"); return; } + ListView lv = getListView(); LayoutInflater inflater = (LayoutInflater) getActivity().getSystemService(Context.LAYOUT_INFLATER_SERVICE); - View header = inflater.inflate(R.layout.feeditemlist_header, null); - getListView().addHeaderView(header); + View header = inflater.inflate(R.layout.feeditemlist_header, lv, false); + lv.addHeaderView(header); TextView txtvTitle = (TextView) header.findViewById(R.id.txtvTitle); TextView txtvAuthor = (TextView) header.findViewById(R.id.txtvAuthor); diff --git a/src/de/danoeh/antennapod/fragment/NewEpisodesFragment.java b/src/de/danoeh/antennapod/fragment/NewEpisodesFragment.java index a0861779c..fe995256b 100644 --- a/src/de/danoeh/antennapod/fragment/NewEpisodesFragment.java +++ b/src/de/danoeh/antennapod/fragment/NewEpisodesFragment.java @@ -34,6 +34,7 @@ import de.danoeh.antennapod.storage.DBWriter; import de.danoeh.antennapod.storage.DownloadRequester; import de.danoeh.antennapod.util.QueueAccess; import de.danoeh.antennapod.util.menuhandler.MenuItemUtils; +import de.danoeh.antennapod.util.menuhandler.NavDrawerActivity; import java.util.List; import java.util.concurrent.atomic.AtomicReference; @@ -138,31 +139,35 @@ public class NewEpisodesFragment extends Fragment { @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { super.onCreateOptionsMenu(menu, inflater); - inflater.inflate(R.menu.new_episodes, menu); - - final SearchView sv = new SearchView(getActivity()); - MenuItemUtils.addSearchItem(menu, sv); - sv.setQueryHint(getString(R.string.search_hint)); - sv.setOnQueryTextListener(new SearchView.OnQueryTextListener() { - @Override - public boolean onQueryTextSubmit(String s) { - sv.clearFocus(); - ((MainActivity) getActivity()).loadChildFragment(SearchFragment.newInstance(s)); - return true; - } + if (itemsLoaded && !MenuItemUtils.isActivityDrawerOpen((NavDrawerActivity) getActivity())) { + inflater.inflate(R.menu.new_episodes, menu); + + final SearchView sv = new SearchView(getActivity()); + MenuItemUtils.addSearchItem(menu, sv); + sv.setQueryHint(getString(R.string.search_hint)); + sv.setOnQueryTextListener(new SearchView.OnQueryTextListener() { + @Override + public boolean onQueryTextSubmit(String s) { + sv.clearFocus(); + ((MainActivity) getActivity()).loadChildFragment(SearchFragment.newInstance(s)); + return true; + } - @Override - public boolean onQueryTextChange(String s) { - return false; - } - }); + @Override + public boolean onQueryTextChange(String s) { + return false; + } + }); + } } @Override public void onPrepareOptionsMenu(Menu menu) { super.onPrepareOptionsMenu(menu); - menu.findItem(R.id.mark_all_read_item).setVisible(unreadItems != null && !unreadItems.isEmpty()); - menu.findItem(R.id.episode_filter_item).setChecked(showOnlyNewEpisodes); + if (itemsLoaded && !MenuItemUtils.isActivityDrawerOpen((NavDrawerActivity) getActivity())) { + menu.findItem(R.id.mark_all_read_item).setVisible(unreadItems != null && !unreadItems.isEmpty()); + menu.findItem(R.id.episode_filter_item).setChecked(showOnlyNewEpisodes); + } } @Override diff --git a/src/de/danoeh/antennapod/fragment/PlaybackHistoryFragment.java b/src/de/danoeh/antennapod/fragment/PlaybackHistoryFragment.java index bab5143d5..470186180 100644 --- a/src/de/danoeh/antennapod/fragment/PlaybackHistoryFragment.java +++ b/src/de/danoeh/antennapod/fragment/PlaybackHistoryFragment.java @@ -25,6 +25,8 @@ import de.danoeh.antennapod.service.download.Downloader; import de.danoeh.antennapod.storage.DBReader; import de.danoeh.antennapod.storage.DBWriter; import de.danoeh.antennapod.util.QueueAccess; +import de.danoeh.antennapod.util.menuhandler.MenuItemUtils; +import de.danoeh.antennapod.util.menuhandler.NavDrawerActivity; import java.util.List; import java.util.concurrent.atomic.AtomicReference; @@ -129,17 +131,21 @@ public class PlaybackHistoryFragment extends ListFragment { @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { super.onCreateOptionsMenu(menu, inflater); - MenuItem clearHistory = menu.add(Menu.NONE, R.id.clear_history_item, Menu.CATEGORY_CONTAINER, R.string.clear_history_label); - MenuItemCompat.setShowAsAction(clearHistory, MenuItemCompat.SHOW_AS_ACTION_IF_ROOM); - TypedArray drawables = getActivity().obtainStyledAttributes(new int[] {R.attr.content_discard}); - clearHistory.setIcon(drawables.getDrawable(0)); - drawables.recycle(); + if (itemsLoaded && !MenuItemUtils.isActivityDrawerOpen((NavDrawerActivity) getActivity())) { + MenuItem clearHistory = menu.add(Menu.NONE, R.id.clear_history_item, Menu.CATEGORY_CONTAINER, R.string.clear_history_label); + MenuItemCompat.setShowAsAction(clearHistory, MenuItemCompat.SHOW_AS_ACTION_IF_ROOM); + TypedArray drawables = getActivity().obtainStyledAttributes(new int[]{R.attr.content_discard}); + clearHistory.setIcon(drawables.getDrawable(0)); + drawables.recycle(); + } } @Override public void onPrepareOptionsMenu(Menu menu) { super.onPrepareOptionsMenu(menu); - menu.findItem(R.id.clear_history_item).setVisible(playbackHistory != null && !playbackHistory.isEmpty()); + if (itemsLoaded && !MenuItemUtils.isActivityDrawerOpen((NavDrawerActivity) getActivity())) { + menu.findItem(R.id.clear_history_item).setVisible(playbackHistory != null && !playbackHistory.isEmpty()); + } } @Override diff --git a/src/de/danoeh/antennapod/fragment/QueueFragment.java b/src/de/danoeh/antennapod/fragment/QueueFragment.java index e3f0bb19d..2f322f75b 100644 --- a/src/de/danoeh/antennapod/fragment/QueueFragment.java +++ b/src/de/danoeh/antennapod/fragment/QueueFragment.java @@ -5,15 +5,25 @@ import android.content.Context; import android.os.AsyncTask; import android.os.Bundle; import android.os.Handler; -import android.os.Parcelable; import android.support.v4.app.Fragment; import android.support.v7.widget.SearchView; import android.util.Log; -import android.view.*; +import android.view.ContextMenu; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; import android.widget.AdapterView; import android.widget.ProgressBar; import android.widget.TextView; + import com.mobeta.android.dslv.DragSortListView; + +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; + import de.danoeh.antennapod.R; import de.danoeh.antennapod.activity.MainActivity; import de.danoeh.antennapod.adapter.DefaultActionButtonCallback; @@ -27,12 +37,8 @@ import de.danoeh.antennapod.service.download.Downloader; import de.danoeh.antennapod.storage.DBReader; import de.danoeh.antennapod.storage.DBWriter; import de.danoeh.antennapod.util.QueueAccess; -import de.danoeh.antennapod.util.UndoBarController; -import de.danoeh.antennapod.util.gui.FeedItemUndoToken; import de.danoeh.antennapod.util.menuhandler.MenuItemUtils; - -import java.util.List; -import java.util.concurrent.atomic.AtomicReference; +import de.danoeh.antennapod.util.menuhandler.NavDrawerActivity; /** * Shows all items in the queue @@ -47,7 +53,6 @@ public class QueueFragment extends Fragment { private QueueListAdapter listAdapter; private TextView txtvEmpty; private ProgressBar progLoading; - private UndoBarController undoBarController; private List<FeedItem> queue; private List<Downloader> downloaderList; @@ -111,7 +116,6 @@ public class QueueFragment extends Fragment { private void resetViewState() { unregisterForContextMenu(listView); listAdapter = null; - undoBarController = null; activity.set(null); viewsCreated = false; blockDownloadObserverUpdate = false; @@ -133,22 +137,24 @@ public class QueueFragment extends Fragment { @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { super.onCreateOptionsMenu(menu, inflater); - final SearchView sv = new SearchView(getActivity()); - MenuItemUtils.addSearchItem(menu, sv); - sv.setQueryHint(getString(R.string.search_hint)); - sv.setOnQueryTextListener(new SearchView.OnQueryTextListener() { - @Override - public boolean onQueryTextSubmit(String s) { - sv.clearFocus(); - ((MainActivity) getActivity()).loadChildFragment(SearchFragment.newInstance(s)); - return true; - } + if (itemsLoaded && !MenuItemUtils.isActivityDrawerOpen((NavDrawerActivity) getActivity())) { + final SearchView sv = new SearchView(getActivity()); + MenuItemUtils.addSearchItem(menu, sv); + sv.setQueryHint(getString(R.string.search_hint)); + sv.setOnQueryTextListener(new SearchView.OnQueryTextListener() { + @Override + public boolean onQueryTextSubmit(String s) { + sv.clearFocus(); + ((MainActivity) getActivity()).loadChildFragment(SearchFragment.newInstance(s)); + return true; + } - @Override - public boolean onQueryTextChange(String s) { - return false; - } - }); + @Override + public boolean onQueryTextChange(String s) { + return false; + } + }); + } } @Override @@ -185,6 +191,9 @@ public class QueueFragment extends Fragment { case R.id.move_to_bottom_item: DBWriter.moveQueueItemToBottom(getActivity(), selectedItem.getId(), true); return true; + case R.id.remove_from_queue_item: + DBWriter.removeQueueItem(getActivity(), selectedItem.getId(), false); + return true; default: return super.onContextItemSelected(item); } @@ -212,19 +221,6 @@ public class QueueFragment extends Fragment { } }); - undoBarController = new UndoBarController(root.findViewById(R.id.undobar), new UndoBarController.UndoListener() { - @Override - public void onUndo(Parcelable token) { - // Perform the undo - FeedItemUndoToken undoToken = (FeedItemUndoToken) token; - if (token != null) { - long itemId = undoToken.getFeedItemId(); - int position = undoToken.getPosition(); - DBWriter.addQueueItemAt(getActivity(), itemId, position, false); - } - } - }); - listView.setDragSortListener(new DragSortListView.DragSortListener() { @Override public void drag(int from, int to) { @@ -245,13 +241,6 @@ public class QueueFragment extends Fragment { @Override public void remove(int which) { - stopItemLoader(); - FeedItem item = (FeedItem) listView.getAdapter().getItem(which); - DBWriter.removeQueueItem(getActivity(), item.getId(), true); - undoBarController.showUndoBar(false, - getString(R.string.removed_from_queue), new FeedItemUndoToken(item, - which) - ); } }); diff --git a/src/de/danoeh/antennapod/fragment/SearchFragment.java b/src/de/danoeh/antennapod/fragment/SearchFragment.java index f89e44717..b3ade4d70 100644 --- a/src/de/danoeh/antennapod/fragment/SearchFragment.java +++ b/src/de/danoeh/antennapod/fragment/SearchFragment.java @@ -20,6 +20,8 @@ import de.danoeh.antennapod.feed.*; import de.danoeh.antennapod.storage.DBReader; import de.danoeh.antennapod.storage.FeedSearcher; import de.danoeh.antennapod.util.QueueAccess; +import de.danoeh.antennapod.util.menuhandler.MenuItemUtils; +import de.danoeh.antennapod.util.menuhandler.NavDrawerActivity; import java.util.List; @@ -131,26 +133,28 @@ public class SearchFragment extends ListFragment { @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { super.onCreateOptionsMenu(menu, inflater); - MenuItem item = menu.add(Menu.NONE, R.id.search_item, Menu.NONE, R.string.search_label); - MenuItemCompat.setShowAsAction(item, MenuItemCompat.SHOW_AS_ACTION_IF_ROOM); - final SearchView sv = new SearchView(getActivity()); - sv.setQueryHint(getString(R.string.search_hint)); - sv.setQuery(getArguments().getString(ARG_QUERY), false); - sv.setOnQueryTextListener(new SearchView.OnQueryTextListener() { - @Override - public boolean onQueryTextSubmit(String s) { - getArguments().putString(ARG_QUERY, s); - itemsLoaded = false; - startSearchTask(); - return true; - } + if (itemsLoaded && !MenuItemUtils.isActivityDrawerOpen((NavDrawerActivity) getActivity())) { + MenuItem item = menu.add(Menu.NONE, R.id.search_item, Menu.NONE, R.string.search_label); + MenuItemCompat.setShowAsAction(item, MenuItemCompat.SHOW_AS_ACTION_IF_ROOM); + final SearchView sv = new SearchView(getActivity()); + sv.setQueryHint(getString(R.string.search_hint)); + sv.setQuery(getArguments().getString(ARG_QUERY), false); + sv.setOnQueryTextListener(new SearchView.OnQueryTextListener() { + @Override + public boolean onQueryTextSubmit(String s) { + getArguments().putString(ARG_QUERY, s); + itemsLoaded = false; + startSearchTask(); + return true; + } - @Override - public boolean onQueryTextChange(String s) { - return false; - } - }); - MenuItemCompat.setActionView(item, sv); + @Override + public boolean onQueryTextChange(String s) { + return false; + } + }); + MenuItemCompat.setActionView(item, sv); + } } private final EventDistributor.EventListener contentUpdate = new EventDistributor.EventListener() { diff --git a/src/de/danoeh/antennapod/fragment/gpodnet/PodcastListFragment.java b/src/de/danoeh/antennapod/fragment/gpodnet/PodcastListFragment.java index 837df0594..1b4616207 100644 --- a/src/de/danoeh/antennapod/fragment/gpodnet/PodcastListFragment.java +++ b/src/de/danoeh/antennapod/fragment/gpodnet/PodcastListFragment.java @@ -21,6 +21,7 @@ import de.danoeh.antennapod.gpoddernet.GpodnetService; import de.danoeh.antennapod.gpoddernet.GpodnetServiceException; import de.danoeh.antennapod.gpoddernet.model.GpodnetPodcast; import de.danoeh.antennapod.util.menuhandler.MenuItemUtils; +import de.danoeh.antennapod.util.menuhandler.NavDrawerActivity; import java.util.List; @@ -44,22 +45,24 @@ public abstract class PodcastListFragment extends Fragment { @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { super.onCreateOptionsMenu(menu, inflater); - final android.support.v7.widget.SearchView sv = new android.support.v7.widget.SearchView(getActivity()); - MenuItemUtils.addSearchItem(menu, sv); - sv.setQueryHint(getString(R.string.gpodnet_search_hint)); - sv.setOnQueryTextListener(new android.support.v7.widget.SearchView.OnQueryTextListener() { - @Override - public boolean onQueryTextSubmit(String s) { - sv.clearFocus(); - ((MainActivity) getActivity()).loadChildFragment(SearchListFragment.newInstance(s)); - return true; - } + if (!MenuItemUtils.isActivityDrawerOpen((NavDrawerActivity) getActivity())) { + final android.support.v7.widget.SearchView sv = new android.support.v7.widget.SearchView(getActivity()); + MenuItemUtils.addSearchItem(menu, sv); + sv.setQueryHint(getString(R.string.gpodnet_search_hint)); + sv.setOnQueryTextListener(new android.support.v7.widget.SearchView.OnQueryTextListener() { + @Override + public boolean onQueryTextSubmit(String s) { + sv.clearFocus(); + ((MainActivity) getActivity()).loadChildFragment(SearchListFragment.newInstance(s)); + return true; + } - @Override - public boolean onQueryTextChange(String s) { - return false; - } - }); + @Override + public boolean onQueryTextChange(String s) { + return false; + } + }); + } } @Override @@ -123,7 +126,7 @@ public abstract class PodcastListFragment extends Fragment { protected void onPostExecute(List<GpodnetPodcast> gpodnetPodcasts) { super.onPostExecute(gpodnetPodcasts); final Context context = getActivity(); - if (context != null && gpodnetPodcasts != null) { + if (context != null && gpodnetPodcasts != null && gpodnetPodcasts.size() > 0) { PodcastListAdapter listAdapter = new PodcastListAdapter(context, 0, gpodnetPodcasts); gridView.setAdapter(listAdapter); listAdapter.notifyDataSetChanged(); @@ -132,13 +135,18 @@ public abstract class PodcastListFragment extends Fragment { gridView.setVisibility(View.VISIBLE); txtvError.setVisibility(View.GONE); butRetry.setVisibility(View.GONE); + } else if (context != null && gpodnetPodcasts != null) { + gridView.setVisibility(View.GONE); + progressBar.setVisibility(View.GONE); + txtvError.setText(getString(R.string.search_status_no_results)); + txtvError.setVisibility(View.VISIBLE); + butRetry.setVisibility(View.GONE); } else if (context != null) { gridView.setVisibility(View.GONE); progressBar.setVisibility(View.GONE); txtvError.setText(getString(R.string.error_msg_prefix) + exception.getMessage()); txtvError.setVisibility(View.VISIBLE); butRetry.setVisibility(View.VISIBLE); - } } diff --git a/src/de/danoeh/antennapod/fragment/gpodnet/SearchListFragment.java b/src/de/danoeh/antennapod/fragment/gpodnet/SearchListFragment.java index 79d0c5d6f..801024787 100644 --- a/src/de/danoeh/antennapod/fragment/gpodnet/SearchListFragment.java +++ b/src/de/danoeh/antennapod/fragment/gpodnet/SearchListFragment.java @@ -4,15 +4,17 @@ import android.os.Bundle; import android.support.v7.widget.SearchView; import android.view.Menu; import android.view.MenuInflater; + +import org.apache.commons.lang3.Validate; + +import java.util.List; + import de.danoeh.antennapod.R; -import de.danoeh.antennapod.activity.MainActivity; -import de.danoeh.antennapod.fragment.SearchFragment; import de.danoeh.antennapod.gpoddernet.GpodnetService; import de.danoeh.antennapod.gpoddernet.GpodnetServiceException; import de.danoeh.antennapod.gpoddernet.model.GpodnetPodcast; import de.danoeh.antennapod.util.menuhandler.MenuItemUtils; - -import java.util.List; +import de.danoeh.antennapod.util.menuhandler.NavDrawerActivity; /** * Performs a search on the gpodder.net directory and displays the results. @@ -44,22 +46,24 @@ public class SearchListFragment extends PodcastListFragment { @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { final SearchView sv = new SearchView(getActivity()); - MenuItemUtils.addSearchItem(menu, sv); - sv.setQueryHint(getString(R.string.gpodnet_search_hint)); - sv.setQuery(query, false); - sv.setOnQueryTextListener(new SearchView.OnQueryTextListener() { - @Override - public boolean onQueryTextSubmit(String s) { - sv.clearFocus(); - changeQuery(s); - return true; - } + if (!MenuItemUtils.isActivityDrawerOpen((NavDrawerActivity) getActivity())) { + MenuItemUtils.addSearchItem(menu, sv); + sv.setQueryHint(getString(R.string.gpodnet_search_hint)); + sv.setQuery(query, false); + sv.setOnQueryTextListener(new SearchView.OnQueryTextListener() { + @Override + public boolean onQueryTextSubmit(String s) { + sv.clearFocus(); + changeQuery(s); + return true; + } - @Override - public boolean onQueryTextChange(String s) { - return false; - } - }); + @Override + public boolean onQueryTextChange(String s) { + return false; + } + }); + } } @Override @@ -68,9 +72,8 @@ public class SearchListFragment extends PodcastListFragment { } public void changeQuery(String query) { - if (query == null) { - throw new NullPointerException(); - } + Validate.notNull(query); + this.query = query; loadData(); } diff --git a/src/de/danoeh/antennapod/fragment/gpodnet/TagFragment.java b/src/de/danoeh/antennapod/fragment/gpodnet/TagFragment.java index f016290bf..204dda992 100644 --- a/src/de/danoeh/antennapod/fragment/gpodnet/TagFragment.java +++ b/src/de/danoeh/antennapod/fragment/gpodnet/TagFragment.java @@ -1,6 +1,9 @@ package de.danoeh.antennapod.fragment.gpodnet; import android.os.Bundle; + +import org.apache.commons.lang3.Validate; + import de.danoeh.antennapod.activity.MainActivity; import de.danoeh.antennapod.gpoddernet.GpodnetService; import de.danoeh.antennapod.gpoddernet.GpodnetServiceException; @@ -21,7 +24,7 @@ public class TagFragment extends PodcastListFragment { private GpodnetTag tag; public static TagFragment newInstance(String tagName) { - if (tagName == null) throw new IllegalArgumentException("tagName = null"); + Validate.notNull(tagName); TagFragment fragment = new TagFragment(); Bundle args = new Bundle(); args.putString("tag", tagName); @@ -34,7 +37,7 @@ public class TagFragment extends PodcastListFragment { super.onCreate(savedInstanceState); Bundle args = getArguments(); - if (args == null || args.getString("tag") == null) throw new IllegalArgumentException("args invalid"); + Validate.isTrue(args != null && args.getString("tag") != null, "args invalid"); tag = new GpodnetTag(args.getString("tag")); ((MainActivity) getActivity()).getMainActivtyActionBar().setTitle(tag.getName()); diff --git a/src/de/danoeh/antennapod/fragment/gpodnet/TagListFragment.java b/src/de/danoeh/antennapod/fragment/gpodnet/TagListFragment.java index 880726e50..2289862aa 100644 --- a/src/de/danoeh/antennapod/fragment/gpodnet/TagListFragment.java +++ b/src/de/danoeh/antennapod/fragment/gpodnet/TagListFragment.java @@ -11,16 +11,17 @@ import android.view.View; import android.widget.AdapterView; import android.widget.ArrayAdapter; import android.widget.TextView; + +import java.util.ArrayList; +import java.util.List; + import de.danoeh.antennapod.R; import de.danoeh.antennapod.activity.MainActivity; -import de.danoeh.antennapod.fragment.SearchFragment; import de.danoeh.antennapod.gpoddernet.GpodnetService; import de.danoeh.antennapod.gpoddernet.GpodnetServiceException; import de.danoeh.antennapod.gpoddernet.model.GpodnetTag; import de.danoeh.antennapod.util.menuhandler.MenuItemUtils; - -import java.util.ArrayList; -import java.util.List; +import de.danoeh.antennapod.util.menuhandler.NavDrawerActivity; public class TagListFragment extends ListFragment { private static final String TAG = "TagListFragment"; @@ -35,22 +36,24 @@ public class TagListFragment extends ListFragment { @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { super.onCreateOptionsMenu(menu, inflater); - final SearchView sv = new SearchView(getActivity()); - MenuItemUtils.addSearchItem(menu, sv); - sv.setQueryHint(getString(R.string.gpodnet_search_hint)); - sv.setOnQueryTextListener(new SearchView.OnQueryTextListener() { - @Override - public boolean onQueryTextSubmit(String s) { - sv.clearFocus(); - ((MainActivity) getActivity()).loadChildFragment(SearchListFragment.newInstance(s)); - return true; - } + if (!MenuItemUtils.isActivityDrawerOpen((NavDrawerActivity) getActivity())) { + final SearchView sv = new SearchView(getActivity()); + MenuItemUtils.addSearchItem(menu, sv); + sv.setQueryHint(getString(R.string.gpodnet_search_hint)); + sv.setOnQueryTextListener(new SearchView.OnQueryTextListener() { + @Override + public boolean onQueryTextSubmit(String s) { + sv.clearFocus(); + ((MainActivity) getActivity()).loadChildFragment(SearchListFragment.newInstance(s)); + return true; + } - @Override - public boolean onQueryTextChange(String s) { - return false; - } - }); + @Override + public boolean onQueryTextChange(String s) { + return false; + } + }); + } } @Override @@ -66,11 +69,26 @@ public class TagListFragment extends ListFragment { } }); - loadData(); + startLoadTask(); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + cancelLoadTask(); + } + + private AsyncTask<Void, Void, List<GpodnetTag>> loadTask; + + private void cancelLoadTask() { + if (loadTask != null && !loadTask.isCancelled()) { + loadTask.cancel(true); + } } - private void loadData() { - AsyncTask<Void, Void, List<GpodnetTag>> task = new AsyncTask<Void, Void, List<GpodnetTag>>() { + private void startLoadTask() { + cancelLoadTask(); + loadTask = new AsyncTask<Void, Void, List<GpodnetTag>>() { private Exception exception; @Override @@ -115,9 +133,9 @@ public class TagListFragment extends ListFragment { } }; if (android.os.Build.VERSION.SDK_INT > android.os.Build.VERSION_CODES.GINGERBREAD_MR1) { - task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + loadTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); } else { - task.execute(); + loadTask.execute(); } } } diff --git a/src/de/danoeh/antennapod/gpoddernet/GpodnetService.java b/src/de/danoeh/antennapod/gpoddernet/GpodnetService.java index a0c5b534c..038b2a367 100644 --- a/src/de/danoeh/antennapod/gpoddernet/GpodnetService.java +++ b/src/de/danoeh/antennapod/gpoddernet/GpodnetService.java @@ -1,8 +1,6 @@ package de.danoeh.antennapod.gpoddernet; -import de.danoeh.antennapod.gpoddernet.model.*; -import de.danoeh.antennapod.preferences.GpodnetPreferences; -import de.danoeh.antennapod.service.download.AntennapodHttpClient; +import org.apache.commons.lang3.Validate; import org.apache.http.Header; import org.apache.http.HttpEntity; import org.apache.http.HttpResponse; @@ -32,6 +30,14 @@ import java.util.Collection; import java.util.LinkedList; import java.util.List; +import de.danoeh.antennapod.gpoddernet.model.GpodnetDevice; +import de.danoeh.antennapod.gpoddernet.model.GpodnetPodcast; +import de.danoeh.antennapod.gpoddernet.model.GpodnetSubscriptionChange; +import de.danoeh.antennapod.gpoddernet.model.GpodnetTag; +import de.danoeh.antennapod.gpoddernet.model.GpodnetUploadChangesResponse; +import de.danoeh.antennapod.preferences.GpodnetPreferences; +import de.danoeh.antennapod.service.download.AntennapodHttpClient; + /** * Communicates with the gpodder.net service. */ @@ -89,10 +95,8 @@ public class GpodnetService { */ 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"); - } + Validate.notNull(tag); + try { URI uri = new URI(BASE_SCHEME, BASE_HOST, String.format( "/api/2/tag/%s/%d.json", tag.getName(), count), null); @@ -120,9 +124,8 @@ public class GpodnetService { */ public List<GpodnetPodcast> getPodcastToplist(int count) throws GpodnetServiceException { - if (count < 1 || count > 100) { - throw new IllegalArgumentException("Count must be in range 1..100"); - } + Validate.isTrue(count >= 1 && count <= 100, "Count must be in range 1..100"); + try { URI uri = new URI(BASE_SCHEME, BASE_HOST, String.format( "/toplist/%d.json", count), null); @@ -155,9 +158,8 @@ public class GpodnetService { * @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"); - } + Validate.isTrue(count >= 1 && count <= 100, "Count must be in range 1..100"); + try { URI uri = new URI(BASE_SCHEME, BASE_HOST, String.format( "/suggestions/%d.json", count), null); @@ -220,9 +222,8 @@ public class GpodnetService { */ public List<GpodnetDevice> getDevices(String username) throws GpodnetServiceException { - if (username == null) { - throw new IllegalArgumentException("Username must not be null"); - } + Validate.notNull(username); + try { URI uri = new URI(BASE_SCHEME, BASE_HOST, String.format( "/api/2/devices/%s.json", username), null); @@ -255,10 +256,9 @@ public class GpodnetService { 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"); - } + Validate.notNull(username); + Validate.notNull(deviceId); + try { URI uri = new URI(BASE_SCHEME, BASE_HOST, String.format( "/api/2/devices/%s/%s.json", username, deviceId), null); @@ -303,10 +303,9 @@ public class GpodnetService { */ 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"); - } + Validate.notNull(username); + Validate.notNull(deviceId); + try { URI uri = new URI(BASE_SCHEME, BASE_HOST, String.format( "/subscriptions/%s/%s.opml", username, deviceId), null); @@ -332,9 +331,8 @@ public class GpodnetService { */ public String getSubscriptionsOfUser(String username) throws GpodnetServiceException { - if (username == null) { - throw new IllegalArgumentException("Username must not be null"); - } + Validate.notNull(username); + try { URI uri = new URI(BASE_SCHEME, BASE_HOST, String.format( "/subscriptions/%s.opml", username), null); @@ -406,10 +404,11 @@ public class GpodnetService { */ 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"); - } + Validate.notNull(username); + Validate.notNull(deviceId); + Validate.notNull(added); + Validate.notNull(removed); + try { URI uri = new URI(BASE_SCHEME, BASE_HOST, String.format( "/api/2/subscriptions/%s/%s.json", username, deviceId), null); @@ -453,10 +452,9 @@ public class GpodnetService { */ 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"); - } + Validate.notNull(username); + Validate.notNull(deviceId); + String params = String.format("since=%d", timestamp); String path = String.format("/api/2/subscriptions/%s/%s.json", username, deviceId); @@ -486,10 +484,9 @@ public class GpodnetService { */ public void authenticate(String username, String password) throws GpodnetServiceException { - if (username == null || password == null) { - throw new IllegalArgumentException( - "Username and password must not be null"); - } + Validate.notNull(username); + Validate.notNull(password); + URI uri; try { uri = new URI(BASE_SCHEME, BASE_HOST, String.format( @@ -517,9 +514,8 @@ public class GpodnetService { private String executeRequest(HttpRequestBase request) throws GpodnetServiceException { - if (request == null) { - throw new IllegalArgumentException("request must not be null"); - } + Validate.notNull(request); + String responseString = null; HttpResponse response = null; try { @@ -586,9 +582,8 @@ public class GpodnetService { private String getStringFromEntity(HttpEntity entity) throws GpodnetServiceException { - if (entity == null) { - throw new IllegalArgumentException("entity must not be null"); - } + Validate.notNull(entity); + ByteArrayOutputStream outputStream; int contentLength = (int) entity.getContentLength(); if (contentLength > 0) { @@ -613,9 +608,7 @@ public class GpodnetService { private void checkStatusCode(HttpResponse response) throws GpodnetServiceException { - if (response == null) { - throw new IllegalArgumentException("response must not be null"); - } + Validate.notNull(response); int responseCode = response.getStatusLine().getStatusCode(); if (responseCode != HttpStatus.SC_OK) { if (responseCode == HttpStatus.SC_UNAUTHORIZED) { @@ -629,9 +622,8 @@ public class GpodnetService { private List<GpodnetPodcast> readPodcastListFromJSONArray(JSONArray array) throws JSONException { - if (array == null) { - throw new IllegalArgumentException("array must not be null"); - } + Validate.notNull(array); + List<GpodnetPodcast> result = new ArrayList<GpodnetPodcast>( array.length()); for (int i = 0; i < array.length(); i++) { @@ -685,9 +677,8 @@ public class GpodnetService { private List<GpodnetDevice> readDeviceListFromJSONArray(JSONArray array) throws JSONException { - if (array == null) { - throw new IllegalArgumentException("array must not be null"); - } + Validate.notNull(array); + List<GpodnetDevice> result = new ArrayList<GpodnetDevice>( array.length()); for (int i = 0; i < array.length(); i++) { @@ -707,9 +698,8 @@ public class GpodnetService { private GpodnetSubscriptionChange readSubscriptionChangesFromJSONObject( JSONObject object) throws JSONException { - if (object == null) { - throw new IllegalArgumentException("object must not be null"); - } + Validate.notNull(object); + List<String> added = new LinkedList<String>(); JSONArray jsonAdded = object.getJSONArray("add"); for (int i = 0; i < jsonAdded.length(); i++) { diff --git a/src/de/danoeh/antennapod/gpoddernet/model/GpodnetDevice.java b/src/de/danoeh/antennapod/gpoddernet/model/GpodnetDevice.java index ae7199fcc..86a2171fa 100644 --- a/src/de/danoeh/antennapod/gpoddernet/model/GpodnetDevice.java +++ b/src/de/danoeh/antennapod/gpoddernet/model/GpodnetDevice.java @@ -1,5 +1,7 @@ package de.danoeh.antennapod.gpoddernet.model; +import org.apache.commons.lang3.Validate; + public class GpodnetDevice { private String id; @@ -9,9 +11,7 @@ public class GpodnetDevice { public GpodnetDevice(String id, String caption, String type, int subscriptions) { - if (id == null) { - throw new IllegalArgumentException("ID must not be null"); - } + Validate.notNull(id); this.id = id; this.caption = caption; diff --git a/src/de/danoeh/antennapod/gpoddernet/model/GpodnetPodcast.java b/src/de/danoeh/antennapod/gpoddernet/model/GpodnetPodcast.java index aa01b66e2..b002035c9 100644 --- a/src/de/danoeh/antennapod/gpoddernet/model/GpodnetPodcast.java +++ b/src/de/danoeh/antennapod/gpoddernet/model/GpodnetPodcast.java @@ -1,5 +1,7 @@ package de.danoeh.antennapod.gpoddernet.model; +import org.apache.commons.lang3.Validate; + public class GpodnetPodcast { private String url; private String title; @@ -11,10 +13,9 @@ public class GpodnetPodcast { 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"); - } + Validate.notNull(url); + Validate.notNull(title); + Validate.notNull(description); this.url = url; this.title = title; diff --git a/src/de/danoeh/antennapod/gpoddernet/model/GpodnetSubscriptionChange.java b/src/de/danoeh/antennapod/gpoddernet/model/GpodnetSubscriptionChange.java index dccb53e5d..a4617118d 100644 --- a/src/de/danoeh/antennapod/gpoddernet/model/GpodnetSubscriptionChange.java +++ b/src/de/danoeh/antennapod/gpoddernet/model/GpodnetSubscriptionChange.java @@ -1,5 +1,7 @@ package de.danoeh.antennapod.gpoddernet.model; +import org.apache.commons.lang3.Validate; + import java.util.List; public class GpodnetSubscriptionChange { @@ -9,10 +11,9 @@ public class GpodnetSubscriptionChange { 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"); - } + Validate.notNull(added); + Validate.notNull(removed); + this.added = added; this.removed = removed; this.timestamp = timestamp; diff --git a/src/de/danoeh/antennapod/gpoddernet/model/GpodnetTag.java b/src/de/danoeh/antennapod/gpoddernet/model/GpodnetTag.java index e8a36a554..80b84095e 100644 --- a/src/de/danoeh/antennapod/gpoddernet/model/GpodnetTag.java +++ b/src/de/danoeh/antennapod/gpoddernet/model/GpodnetTag.java @@ -1,5 +1,7 @@ package de.danoeh.antennapod.gpoddernet.model; +import org.apache.commons.lang3.Validate; + import java.util.Comparator; public class GpodnetTag { @@ -8,9 +10,7 @@ public class GpodnetTag { private int usage; public GpodnetTag(String name, int usage) { - if (name == null) { - throw new IllegalArgumentException("Name must not be null"); - } + Validate.notNull(name); this.name = name; this.usage = usage; diff --git a/src/de/danoeh/antennapod/preferences/PlaybackPreferences.java b/src/de/danoeh/antennapod/preferences/PlaybackPreferences.java index 80e0768dd..1d1ab052f 100644 --- a/src/de/danoeh/antennapod/preferences/PlaybackPreferences.java +++ b/src/de/danoeh/antennapod/preferences/PlaybackPreferences.java @@ -4,6 +4,9 @@ import android.content.Context; import android.content.SharedPreferences; import android.preference.PreferenceManager; import android.util.Log; + +import org.apache.commons.lang3.Validate; + import de.danoeh.antennapod.BuildConfig; /** @@ -66,8 +69,8 @@ public class PlaybackPreferences implements public static void createInstance(Context context) { if (BuildConfig.DEBUG) Log.d(TAG, "Creating new instance of UserPreferences"); - if (context == null) - throw new IllegalArgumentException("Context must not be null"); + Validate.notNull(context); + instance = new PlaybackPreferences(context); PreferenceManager.getDefaultSharedPreferences(context) diff --git a/src/de/danoeh/antennapod/preferences/UserPreferences.java b/src/de/danoeh/antennapod/preferences/UserPreferences.java index 31250bcd9..2020ddfae 100644 --- a/src/de/danoeh/antennapod/preferences/UserPreferences.java +++ b/src/de/danoeh/antennapod/preferences/UserPreferences.java @@ -7,11 +7,9 @@ import android.content.Intent; import android.content.SharedPreferences; import android.preference.PreferenceManager; import android.util.Log; -import de.danoeh.antennapod.BuildConfig; -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.activity.OpmlImportFromPathActivity; -import de.danoeh.antennapod.receiver.FeedUpdateReceiver; + import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; import org.json.JSONArray; import org.json.JSONException; @@ -21,6 +19,11 @@ import java.util.LinkedList; import java.util.List; import java.util.concurrent.TimeUnit; +import de.danoeh.antennapod.BuildConfig; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.activity.OpmlImportFromPathActivity; +import de.danoeh.antennapod.receiver.FeedUpdateReceiver; + /** * Provides access to preferences set by the user in the settings screen. A * private instance of this class must first be instantiated via @@ -28,347 +31,372 @@ import java.util.concurrent.TimeUnit; * when called. */ public class UserPreferences implements - SharedPreferences.OnSharedPreferenceChangeListener { - private static final String TAG = "UserPreferences"; - - public static final String PREF_PAUSE_ON_HEADSET_DISCONNECT = "prefPauseOnHeadsetDisconnect"; - public static final String PREF_FOLLOW_QUEUE = "prefFollowQueue"; - public static final String PREF_DOWNLOAD_MEDIA_ON_WIFI_ONLY = "prefDownloadMediaOnWifiOnly"; - public static final String PREF_UPDATE_INTERVAL = "prefAutoUpdateIntervall"; - public static final String PREF_MOBILE_UPDATE = "prefMobileUpdate"; - public static final String PREF_DISPLAY_ONLY_EPISODES = "prefDisplayOnlyEpisodes"; - public static final String PREF_AUTO_DELETE = "prefAutoDelete"; - public static final String PREF_AUTO_FLATTR = "pref_auto_flattr"; - public static final String PREF_THEME = "prefTheme"; - public static final String PREF_DATA_FOLDER = "prefDataFolder"; - public static final String PREF_ENABLE_AUTODL = "prefEnableAutoDl"; - 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"; - public static final String PREF_PAUSE_PLAYBACK_FOR_FOCUS_LOSS = "prefPauseForFocusLoss"; + SharedPreferences.OnSharedPreferenceChangeListener { + private static final String TAG = "UserPreferences"; + + public static final String PREF_PAUSE_ON_HEADSET_DISCONNECT = "prefPauseOnHeadsetDisconnect"; + public static final String PREF_FOLLOW_QUEUE = "prefFollowQueue"; + public static final String PREF_DOWNLOAD_MEDIA_ON_WIFI_ONLY = "prefDownloadMediaOnWifiOnly"; + public static final String PREF_UPDATE_INTERVAL = "prefAutoUpdateIntervall"; + public static final String PREF_MOBILE_UPDATE = "prefMobileUpdate"; + public static final String PREF_DISPLAY_ONLY_EPISODES = "prefDisplayOnlyEpisodes"; + public static final String PREF_AUTO_DELETE = "prefAutoDelete"; + public static final String PREF_AUTO_FLATTR = "pref_auto_flattr"; + public static final String PREF_AUTO_FLATTR_PLAYED_DURATION_THRESHOLD = "prefAutoFlattrPlayedDurationThreshold"; + public static final String PREF_THEME = "prefTheme"; + public static final String PREF_DATA_FOLDER = "prefDataFolder"; + public static final String PREF_ENABLE_AUTODL = "prefEnableAutoDl"; + 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"; + public static final String PREF_PAUSE_PLAYBACK_FOR_FOCUS_LOSS = "prefPauseForFocusLoss"; + private static final String PREF_SEEK_DELTA_SECS = "prefSeekDeltaSecs"; // TODO: Make this value configurable - private static final double PLAYED_DURATION_AUTOFLATTR_THRESHOLD = 0.8; - - private static int EPISODE_CACHE_SIZE_UNLIMITED = -1; - - private static UserPreferences instance; - private final Context context; - - // Preferences - private boolean pauseOnHeadsetDisconnect; - private boolean followQueue; - private boolean downloadMediaOnWifiOnly; - private long updateInterval; - private boolean allowMobileUpdate; - private boolean displayOnlyEpisodes; - private boolean autoDelete; - private boolean autoFlattr; - private int theme; - private boolean enableAutodownload; - private boolean enableAutodownloadWifiFilter; - private String[] autodownloadSelectedNetworks; - private int episodeCacheSize; - private String playbackSpeed; - private String[] playbackSpeedArray; - private boolean pauseForFocusLoss; - private boolean isFreshInstall; - - private UserPreferences(Context context) { - this.context = context; - loadPreferences(); - } - - /** - * Sets up the UserPreferences class. - * - * @throws IllegalArgumentException - * if context is null - * */ - public static void createInstance(Context context) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Creating new instance of UserPreferences"); - if (context == null) - throw new IllegalArgumentException("Context must not be null"); - instance = new UserPreferences(context); - - createImportDirectory(); - createNoMediaFile(); - PreferenceManager.getDefaultSharedPreferences(context) - .registerOnSharedPreferenceChangeListener(instance); - - } - - private void loadPreferences() { - SharedPreferences sp = PreferenceManager - .getDefaultSharedPreferences(context); - EPISODE_CACHE_SIZE_UNLIMITED = context.getResources().getInteger( - R.integer.episode_cache_size_unlimited); - pauseOnHeadsetDisconnect = sp.getBoolean( - PREF_PAUSE_ON_HEADSET_DISCONNECT, true); - followQueue = sp.getBoolean(PREF_FOLLOW_QUEUE, false); - downloadMediaOnWifiOnly = sp.getBoolean( - PREF_DOWNLOAD_MEDIA_ON_WIFI_ONLY, true); - updateInterval = readUpdateInterval(sp.getString(PREF_UPDATE_INTERVAL, - "0")); - allowMobileUpdate = sp.getBoolean(PREF_MOBILE_UPDATE, false); - displayOnlyEpisodes = sp.getBoolean(PREF_DISPLAY_ONLY_EPISODES, false); - autoDelete = sp.getBoolean(PREF_AUTO_DELETE, false); - autoFlattr = sp.getBoolean(PREF_AUTO_FLATTR, false); - theme = readThemeValue(sp.getString(PREF_THEME, "0")); - enableAutodownloadWifiFilter = sp.getBoolean( - PREF_ENABLE_AUTODL_WIFI_FILTER, false); - autodownloadSelectedNetworks = StringUtils.split( - sp.getString(PREF_AUTODL_SELECTED_NETWORKS, ""), ','); - episodeCacheSize = readEpisodeCacheSizeInternal(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)); - pauseForFocusLoss = sp.getBoolean(PREF_PAUSE_PLAYBACK_FOR_FOCUS_LOSS, false); - } - - private int readThemeValue(String valueFromPrefs) { - switch (Integer.parseInt(valueFromPrefs)) { - case 0: - return R.style.Theme_AntennaPod_Light; - case 1: - return R.style.Theme_AntennaPod_Dark; - default: - return R.style.Theme_AntennaPod_Light; - } - } - - private long readUpdateInterval(String valueFromPrefs) { - int hours = Integer.parseInt(valueFromPrefs); - return TimeUnit.HOURS.toMillis(hours); - } - - private int readEpisodeCacheSizeInternal(String valueFromPrefs) { - if (valueFromPrefs.equals(context - .getString(R.string.pref_episode_cache_unlimited))) { - return EPISODE_CACHE_SIZE_UNLIMITED; - } else { - return Integer.valueOf(valueFromPrefs); - } - } - - 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( - "UserPreferences was used before being set up"); - } - } - - public static boolean isPauseOnHeadsetDisconnect() { - instanceAvailable(); - return instance.pauseOnHeadsetDisconnect; - } - - public static boolean isFollowQueue() { - instanceAvailable(); - return instance.followQueue; - } - - public static boolean isDownloadMediaOnWifiOnly() { - instanceAvailable(); - return instance.downloadMediaOnWifiOnly; - } - - public static long getUpdateInterval() { - instanceAvailable(); - return instance.updateInterval; - } - - public static boolean isAllowMobileUpdate() { - instanceAvailable(); - return instance.allowMobileUpdate; - } - - public static boolean isDisplayOnlyEpisodes() { - instanceAvailable(); - //return instance.displayOnlyEpisodes; + private static final float PREF_AUTO_FLATTR_PLAYED_DURATION_THRESHOLD_DEFAULT = 0.8f; + + private static int EPISODE_CACHE_SIZE_UNLIMITED = -1; + + private static UserPreferences instance; + private final Context context; + + // Preferences + private boolean pauseOnHeadsetDisconnect; + private boolean followQueue; + private boolean downloadMediaOnWifiOnly; + private long updateInterval; + private boolean allowMobileUpdate; + private boolean displayOnlyEpisodes; + private boolean autoDelete; + private boolean autoFlattr; + private float autoFlattrPlayedDurationThreshold; + private int theme; + private boolean enableAutodownload; + private boolean enableAutodownloadWifiFilter; + private String[] autodownloadSelectedNetworks; + private int episodeCacheSize; + private String playbackSpeed; + private String[] playbackSpeedArray; + private boolean pauseForFocusLoss; + private int seekDeltaSecs; + private boolean isFreshInstall; + + private UserPreferences(Context context) { + this.context = context; + loadPreferences(); + } + + /** + * Sets up the UserPreferences class. + * + * @throws IllegalArgumentException if context is null + */ + public static void createInstance(Context context) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Creating new instance of UserPreferences"); + Validate.notNull(context); + + instance = new UserPreferences(context); + + createImportDirectory(); + createNoMediaFile(); + PreferenceManager.getDefaultSharedPreferences(context) + .registerOnSharedPreferenceChangeListener(instance); + + } + + private void loadPreferences() { + SharedPreferences sp = PreferenceManager + .getDefaultSharedPreferences(context); + EPISODE_CACHE_SIZE_UNLIMITED = context.getResources().getInteger( + R.integer.episode_cache_size_unlimited); + pauseOnHeadsetDisconnect = sp.getBoolean( + PREF_PAUSE_ON_HEADSET_DISCONNECT, true); + followQueue = sp.getBoolean(PREF_FOLLOW_QUEUE, false); + downloadMediaOnWifiOnly = sp.getBoolean( + PREF_DOWNLOAD_MEDIA_ON_WIFI_ONLY, true); + updateInterval = readUpdateInterval(sp.getString(PREF_UPDATE_INTERVAL, + "0")); + allowMobileUpdate = sp.getBoolean(PREF_MOBILE_UPDATE, false); + displayOnlyEpisodes = sp.getBoolean(PREF_DISPLAY_ONLY_EPISODES, false); + autoDelete = sp.getBoolean(PREF_AUTO_DELETE, false); + autoFlattr = sp.getBoolean(PREF_AUTO_FLATTR, false); + autoFlattrPlayedDurationThreshold = sp.getFloat(PREF_AUTO_FLATTR_PLAYED_DURATION_THRESHOLD, + PREF_AUTO_FLATTR_PLAYED_DURATION_THRESHOLD_DEFAULT); + theme = readThemeValue(sp.getString(PREF_THEME, "0")); + enableAutodownloadWifiFilter = sp.getBoolean( + PREF_ENABLE_AUTODL_WIFI_FILTER, false); + autodownloadSelectedNetworks = StringUtils.split( + sp.getString(PREF_AUTODL_SELECTED_NETWORKS, ""), ','); + episodeCacheSize = readEpisodeCacheSizeInternal(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)); + pauseForFocusLoss = sp.getBoolean(PREF_PAUSE_PLAYBACK_FOR_FOCUS_LOSS, false); + seekDeltaSecs = Integer.valueOf(sp.getString(PREF_SEEK_DELTA_SECS, "30")); + } + + private int readThemeValue(String valueFromPrefs) { + switch (Integer.parseInt(valueFromPrefs)) { + case 0: + return R.style.Theme_AntennaPod_Light; + case 1: + return R.style.Theme_AntennaPod_Dark; + default: + return R.style.Theme_AntennaPod_Light; + } + } + + private long readUpdateInterval(String valueFromPrefs) { + int hours = Integer.parseInt(valueFromPrefs); + return TimeUnit.HOURS.toMillis(hours); + } + + private int readEpisodeCacheSizeInternal(String valueFromPrefs) { + if (valueFromPrefs.equals(context + .getString(R.string.pref_episode_cache_unlimited))) { + return EPISODE_CACHE_SIZE_UNLIMITED; + } else { + return Integer.valueOf(valueFromPrefs); + } + } + + 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( + "UserPreferences was used before being set up"); + } + } + + public static boolean isPauseOnHeadsetDisconnect() { + instanceAvailable(); + return instance.pauseOnHeadsetDisconnect; + } + + public static boolean isFollowQueue() { + instanceAvailable(); + return instance.followQueue; + } + + public static boolean isDownloadMediaOnWifiOnly() { + instanceAvailable(); + return instance.downloadMediaOnWifiOnly; + } + + public static long getUpdateInterval() { + instanceAvailable(); + return instance.updateInterval; + } + + public static boolean isAllowMobileUpdate() { + instanceAvailable(); + return instance.allowMobileUpdate; + } + + public static boolean isDisplayOnlyEpisodes() { + instanceAvailable(); + //return instance.displayOnlyEpisodes; return false; - } - - public static boolean isAutoDelete() { - instanceAvailable(); - return instance.autoDelete; - } - - public static boolean isAutoFlattr() { - instanceAvailable(); - return instance.autoFlattr; - } - - public static int getTheme() { - instanceAvailable(); - return instance.theme; - } - - public static boolean isEnableAutodownloadWifiFilter() { - instanceAvailable(); - return instance.enableAutodownloadWifiFilter; - } - - public static String[] getAutodownloadSelectedNetworks() { - instanceAvailable(); - return instance.autodownloadSelectedNetworks; - } - - public static int getEpisodeCacheSizeUnlimited() { - 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 - * 'unlimited'. - */ - public static int getEpisodeCacheSize() { - instanceAvailable(); - return instance.episodeCacheSize; - } - - public static boolean isEnableAutodownload() { - instanceAvailable(); - return instance.enableAutodownload; - } - - public static boolean shouldPauseForFocusLoss() { - instanceAvailable(); - return instance.pauseForFocusLoss; - } - - public static boolean isFreshInstall() { - instanceAvailable(); - return instance.isFreshInstall; - } - - @Override - public void onSharedPreferenceChanged(SharedPreferences sp, String key) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Registered change of user preferences. Key: " + key); - - if (key.equals(PREF_DOWNLOAD_MEDIA_ON_WIFI_ONLY)) { - downloadMediaOnWifiOnly = sp.getBoolean( - PREF_DOWNLOAD_MEDIA_ON_WIFI_ONLY, true); - - } else if (key.equals(PREF_MOBILE_UPDATE)) { - allowMobileUpdate = sp.getBoolean(PREF_MOBILE_UPDATE, false); - - } else if (key.equals(PREF_FOLLOW_QUEUE)) { - followQueue = sp.getBoolean(PREF_FOLLOW_QUEUE, false); - - } else if (key.equals(PREF_UPDATE_INTERVAL)) { - updateInterval = readUpdateInterval(sp.getString( - PREF_UPDATE_INTERVAL, "0")); - restartUpdateAlarm(updateInterval); - - } else if (key.equals(PREF_AUTO_DELETE)) { - autoDelete = sp.getBoolean(PREF_AUTO_DELETE, false); - - } else if (key.equals(PREF_AUTO_FLATTR)) { - autoFlattr = sp.getBoolean(PREF_AUTO_FLATTR, false); - } else if (key.equals(PREF_DISPLAY_ONLY_EPISODES)) { - displayOnlyEpisodes = sp.getBoolean(PREF_DISPLAY_ONLY_EPISODES, - false); - } else if (key.equals(PREF_THEME)) { - theme = readThemeValue(sp.getString(PREF_THEME, "")); - } else if (key.equals(PREF_ENABLE_AUTODL_WIFI_FILTER)) { - enableAutodownloadWifiFilter = sp.getBoolean( - PREF_ENABLE_AUTODL_WIFI_FILTER, false); - } else if (key.equals(PREF_AUTODL_SELECTED_NETWORKS)) { - autodownloadSelectedNetworks = StringUtils.split( - sp.getString(PREF_AUTODL_SELECTED_NETWORKS, ""), ','); - } else if (key.equals(PREF_EPISODE_CACHE_SIZE)) { - episodeCacheSize = readEpisodeCacheSizeInternal(sp.getString( - 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)); - } else if (key.equals(PREF_PAUSE_PLAYBACK_FOR_FOCUS_LOSS)) { - pauseForFocusLoss = sp.getBoolean(PREF_PAUSE_PLAYBACK_FOR_FOCUS_LOSS, false); - } else if (key.equals(PREF_PAUSE_ON_HEADSET_DISCONNECT)) { + } + + public static boolean isAutoDelete() { + instanceAvailable(); + return instance.autoDelete; + } + + public static boolean isAutoFlattr() { + instanceAvailable(); + return instance.autoFlattr; + } + + /** + * Returns the time after which an episode should be auto-flattr'd in percent of the episode's + * duration. + */ + public static float getAutoFlattrPlayedDurationThreshold() { + instanceAvailable(); + return instance.autoFlattrPlayedDurationThreshold; + } + + public static int getTheme() { + instanceAvailable(); + return instance.theme; + } + + public static boolean isEnableAutodownloadWifiFilter() { + instanceAvailable(); + return instance.enableAutodownloadWifiFilter; + } + + public static String[] getAutodownloadSelectedNetworks() { + instanceAvailable(); + return instance.autodownloadSelectedNetworks; + } + + public static int getEpisodeCacheSizeUnlimited() { + return EPISODE_CACHE_SIZE_UNLIMITED; + } + + public static String getPlaybackSpeed() { + instanceAvailable(); + return instance.playbackSpeed; + } + + public static String[] getPlaybackSpeedArray() { + instanceAvailable(); + return instance.playbackSpeedArray; + } + + public static int getSeekDeltaMs() { + instanceAvailable(); + return 1000 * instance.seekDeltaSecs; + } + + /** + * 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 + * 'unlimited'. + */ + public static int getEpisodeCacheSize() { + instanceAvailable(); + return instance.episodeCacheSize; + } + + public static boolean isEnableAutodownload() { + instanceAvailable(); + return instance.enableAutodownload; + } + + public static boolean shouldPauseForFocusLoss() { + instanceAvailable(); + return instance.pauseForFocusLoss; + } + + public static boolean isFreshInstall() { + instanceAvailable(); + return instance.isFreshInstall; + } + + @Override + public void onSharedPreferenceChanged(SharedPreferences sp, String key) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Registered change of user preferences. Key: " + key); + + if (key.equals(PREF_DOWNLOAD_MEDIA_ON_WIFI_ONLY)) { + downloadMediaOnWifiOnly = sp.getBoolean( + PREF_DOWNLOAD_MEDIA_ON_WIFI_ONLY, true); + + } else if (key.equals(PREF_MOBILE_UPDATE)) { + allowMobileUpdate = sp.getBoolean(PREF_MOBILE_UPDATE, false); + + } else if (key.equals(PREF_FOLLOW_QUEUE)) { + followQueue = sp.getBoolean(PREF_FOLLOW_QUEUE, false); + + } else if (key.equals(PREF_UPDATE_INTERVAL)) { + updateInterval = readUpdateInterval(sp.getString( + PREF_UPDATE_INTERVAL, "0")); + restartUpdateAlarm(updateInterval); + + } else if (key.equals(PREF_AUTO_DELETE)) { + autoDelete = sp.getBoolean(PREF_AUTO_DELETE, false); + + } else if (key.equals(PREF_AUTO_FLATTR)) { + autoFlattr = sp.getBoolean(PREF_AUTO_FLATTR, false); + } else if (key.equals(PREF_DISPLAY_ONLY_EPISODES)) { + displayOnlyEpisodes = sp.getBoolean(PREF_DISPLAY_ONLY_EPISODES, + false); + } else if (key.equals(PREF_THEME)) { + theme = readThemeValue(sp.getString(PREF_THEME, "")); + } else if (key.equals(PREF_ENABLE_AUTODL_WIFI_FILTER)) { + enableAutodownloadWifiFilter = sp.getBoolean( + PREF_ENABLE_AUTODL_WIFI_FILTER, false); + } else if (key.equals(PREF_AUTODL_SELECTED_NETWORKS)) { + autodownloadSelectedNetworks = StringUtils.split( + sp.getString(PREF_AUTODL_SELECTED_NETWORKS, ""), ','); + } else if (key.equals(PREF_EPISODE_CACHE_SIZE)) { + episodeCacheSize = readEpisodeCacheSizeInternal(sp.getString( + 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)); + } else if (key.equals(PREF_PAUSE_PLAYBACK_FOR_FOCUS_LOSS)) { + pauseForFocusLoss = sp.getBoolean(PREF_PAUSE_PLAYBACK_FOR_FOCUS_LOSS, false); + } else if (key.equals(PREF_SEEK_DELTA_SECS)) { + seekDeltaSecs = Integer.valueOf(sp.getString(PREF_SEEK_DELTA_SECS, "30")); + } else if (key.equals(PREF_PAUSE_ON_HEADSET_DISCONNECT)) { pauseOnHeadsetDisconnect = sp.getBoolean(PREF_PAUSE_ON_HEADSET_DISCONNECT, true); + } else if (key.equals(PREF_AUTO_FLATTR_PLAYED_DURATION_THRESHOLD)) { + autoFlattrPlayedDurationThreshold = sp.getFloat(PREF_AUTO_FLATTR_PLAYED_DURATION_THRESHOLD, + PREF_AUTO_FLATTR_PLAYED_DURATION_THRESHOLD_DEFAULT); } - } - - 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, - String[] value) { - SharedPreferences.Editor editor = PreferenceManager - .getDefaultSharedPreferences(context.getApplicationContext()) - .edit(); - editor.putString(PREF_AUTODL_SELECTED_NETWORKS, - StringUtils.join(value, ',')); - editor.commit(); - } + } + + 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, + String[] value) { + SharedPreferences.Editor editor = PreferenceManager + .getDefaultSharedPreferences(context.getApplicationContext()) + .edit(); + editor.putString(PREF_AUTODL_SELECTED_NETWORKS, + StringUtils.join(value, ',')); + editor.commit(); + } /** - * Sets the update interval value. Should only be used for testing purposes! - * */ + * Sets the update interval value. Should only be used for testing purposes! + */ public static void setUpdateInterval(Context context, long newValue) { instanceAvailable(); SharedPreferences.Editor editor = PreferenceManager @@ -380,154 +408,170 @@ public class UserPreferences implements instance.updateInterval = newValue; } - /** - * Return the folder where the app stores all of its data. This method will - * return the standard data folder if none has been set by the user. - * - * @param type - * The name of the folder inside the data folder. May be null - * when accessing the root of the data folder. - * @return The data folder that has been requested or null if the folder - * could not be created. - */ - public static File getDataFolder(Context context, String type) { - instanceAvailable(); - SharedPreferences prefs = PreferenceManager - .getDefaultSharedPreferences(context.getApplicationContext()); - String strDir = prefs.getString(PREF_DATA_FOLDER, null); - if (strDir == null) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Using default data folder"); - return context.getExternalFilesDir(type); - } else { - File dataDir = new File(strDir); - if (!dataDir.exists()) { - if (!dataDir.mkdir()) { - Log.w(TAG, "Could not create data folder"); - return null; - } - } - - if (type == null) { - return dataDir; - } else { - // handle path separators - String[] dirs = type.split("/"); - for (int i = 0; i < dirs.length; i++) { - if (dirs.length > 0) { - if (i < dirs.length - 1) { - dataDir = getDataFolder(context, dirs[i]); - if (dataDir == null) { - return null; - } - } - type = dirs[i]; - } - } - File typeDir = new File(dataDir, type); - if (!typeDir.exists()) { - if (dataDir.canWrite()) { - if (!typeDir.mkdir()) { - Log.e(TAG, "Could not create data folder named " - + type); - return null; - } - } - } - return typeDir; - } - - } - } - - public static void setDataFolder(String dir) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Result from DirectoryChooser: " + dir); - instanceAvailable(); - SharedPreferences prefs = PreferenceManager - .getDefaultSharedPreferences(instance.context); - SharedPreferences.Editor editor = prefs.edit(); - editor.putString(PREF_DATA_FOLDER, dir); - editor.commit(); - createImportDirectory(); - } - - /** Create a .nomedia file to prevent scanning by the media scanner. */ - private static void createNoMediaFile() { - File f = new File(instance.context.getExternalFilesDir(null), - ".nomedia"); - if (!f.exists()) { - try { - f.createNewFile(); - } catch (IOException e) { - Log.e(TAG, "Could not create .nomedia file"); - e.printStackTrace(); - } - if (BuildConfig.DEBUG) - Log.d(TAG, ".nomedia file created"); - } - } - - /** - * Creates the import directory if it doesn't exist and if storage is - * available - */ - private static void createImportDirectory() { - File importDir = getDataFolder(instance.context, - OpmlImportFromPathActivity.IMPORT_DIR); - if (importDir != null) { - if (importDir.exists()) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Import directory already exists"); - } else { - if (BuildConfig.DEBUG) - Log.d(TAG, "Creating import directory"); - importDir.mkdir(); - } - } else { - if (BuildConfig.DEBUG) - Log.d(TAG, "Could not access external storage."); - } - } - - /** - * Updates alarm registered with the AlarmManager service or deactivates it. - * - * @param millis - * new value to register with AlarmManager. If millis is 0, the - * alarm is deactivated. - * */ - public static void restartUpdateAlarm(long millis) { - instanceAvailable(); - if (BuildConfig.DEBUG) - Log.d(TAG, "Restarting update alarm. New value: " + millis); - AlarmManager alarmManager = (AlarmManager) instance.context - .getSystemService(Context.ALARM_SERVICE); - PendingIntent updateIntent = PendingIntent.getBroadcast( - instance.context, 0, new Intent( - FeedUpdateReceiver.ACTION_REFRESH_FEEDS), 0); - alarmManager.cancel(updateIntent); - if (millis != 0) { - alarmManager.setRepeating(AlarmManager.RTC_WAKEUP, millis, millis, - updateIntent); - if (BuildConfig.DEBUG) - Log.d(TAG, "Changed alarm to new interval"); - } else { - if (BuildConfig.DEBUG) - Log.d(TAG, "Automatic update was deactivated"); - } - } - /** - * Reads episode cache size as it is saved in the episode_cache_size_values array. + * Change the auto-flattr settings + * + * @param context For accessing the shared preferences + * @param enabled Whether automatic flattring should be enabled at all + * @param autoFlattrThreshold The percentage of playback time after which an episode should be + * flattrd. Must be a value between 0 and 1 (inclusive) * */ - public static int readEpisodeCacheSize(String valueFromPrefs) { + public static void setAutoFlattrSettings(Context context, boolean enabled, float autoFlattrThreshold) { instanceAvailable(); - return instance.readEpisodeCacheSizeInternal(valueFromPrefs); + Validate.inclusiveBetween(0.0, 1.0, autoFlattrThreshold); + PreferenceManager.getDefaultSharedPreferences(context.getApplicationContext()) + .edit() + .putBoolean(PREF_AUTO_FLATTR, enabled) + .putFloat(PREF_AUTO_FLATTR_PLAYED_DURATION_THRESHOLD, autoFlattrThreshold) + .commit(); + instance.autoFlattr = enabled; + instance.autoFlattrPlayedDurationThreshold = autoFlattrThreshold; + } + + /** + * Return the folder where the app stores all of its data. This method will + * return the standard data folder if none has been set by the user. + * + * @param type The name of the folder inside the data folder. May be null + * when accessing the root of the data folder. + * @return The data folder that has been requested or null if the folder + * could not be created. + */ + public static File getDataFolder(Context context, String type) { + instanceAvailable(); + SharedPreferences prefs = PreferenceManager + .getDefaultSharedPreferences(context.getApplicationContext()); + String strDir = prefs.getString(PREF_DATA_FOLDER, null); + if (strDir == null) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Using default data folder"); + return context.getExternalFilesDir(type); + } else { + File dataDir = new File(strDir); + if (!dataDir.exists()) { + if (!dataDir.mkdir()) { + Log.w(TAG, "Could not create data folder"); + return null; + } + } + + if (type == null) { + return dataDir; + } else { + // handle path separators + String[] dirs = type.split("/"); + for (int i = 0; i < dirs.length; i++) { + if (dirs.length > 0) { + if (i < dirs.length - 1) { + dataDir = getDataFolder(context, dirs[i]); + if (dataDir == null) { + return null; + } + } + type = dirs[i]; + } + } + File typeDir = new File(dataDir, type); + if (!typeDir.exists()) { + if (dataDir.canWrite()) { + if (!typeDir.mkdir()) { + Log.e(TAG, "Could not create data folder named " + + type); + return null; + } + } + } + return typeDir; + } + + } + } + + public static void setDataFolder(String dir) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Result from DirectoryChooser: " + dir); + instanceAvailable(); + SharedPreferences prefs = PreferenceManager + .getDefaultSharedPreferences(instance.context); + SharedPreferences.Editor editor = prefs.edit(); + editor.putString(PREF_DATA_FOLDER, dir); + editor.commit(); + createImportDirectory(); } - public static double getPlayedDurationAutoflattrThreshold() { + /** + * Create a .nomedia file to prevent scanning by the media scanner. + */ + private static void createNoMediaFile() { + File f = new File(instance.context.getExternalFilesDir(null), + ".nomedia"); + if (!f.exists()) { + try { + f.createNewFile(); + } catch (IOException e) { + Log.e(TAG, "Could not create .nomedia file"); + e.printStackTrace(); + } + if (BuildConfig.DEBUG) + Log.d(TAG, ".nomedia file created"); + } + } + + /** + * Creates the import directory if it doesn't exist and if storage is + * available + */ + private static void createImportDirectory() { + File importDir = getDataFolder(instance.context, + OpmlImportFromPathActivity.IMPORT_DIR); + if (importDir != null) { + if (importDir.exists()) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Import directory already exists"); + } else { + if (BuildConfig.DEBUG) + Log.d(TAG, "Creating import directory"); + importDir.mkdir(); + } + } else { + if (BuildConfig.DEBUG) + Log.d(TAG, "Could not access external storage."); + } + } + + /** + * Updates alarm registered with the AlarmManager service or deactivates it. + * + * @param millis new value to register with AlarmManager. If millis is 0, the + * alarm is deactivated. + */ + public static void restartUpdateAlarm(long millis) { instanceAvailable(); - return PLAYED_DURATION_AUTOFLATTR_THRESHOLD; + if (BuildConfig.DEBUG) + Log.d(TAG, "Restarting update alarm. New value: " + millis); + AlarmManager alarmManager = (AlarmManager) instance.context + .getSystemService(Context.ALARM_SERVICE); + PendingIntent updateIntent = PendingIntent.getBroadcast( + instance.context, 0, new Intent( + FeedUpdateReceiver.ACTION_REFRESH_FEEDS), 0 + ); + alarmManager.cancel(updateIntent); + if (millis != 0) { + alarmManager.setRepeating(AlarmManager.RTC_WAKEUP, millis, millis, + updateIntent); + if (BuildConfig.DEBUG) + Log.d(TAG, "Changed alarm to new interval"); + } else { + if (BuildConfig.DEBUG) + Log.d(TAG, "Automatic update was deactivated"); + } + } + + /** + * Reads episode cache size as it is saved in the episode_cache_size_values array. + */ + public static int readEpisodeCacheSize(String valueFromPrefs) { + instanceAvailable(); + return instance.readEpisodeCacheSizeInternal(valueFromPrefs); } } diff --git a/src/de/danoeh/antennapod/receiver/AlarmUpdateReceiver.java b/src/de/danoeh/antennapod/receiver/AlarmUpdateReceiver.java index 7b8879f21..a0539e276 100644 --- a/src/de/danoeh/antennapod/receiver/AlarmUpdateReceiver.java +++ b/src/de/danoeh/antennapod/receiver/AlarmUpdateReceiver.java @@ -4,6 +4,9 @@ import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.util.Log; + +import org.apache.commons.lang3.StringUtils; + import de.danoeh.antennapod.BuildConfig; import de.danoeh.antennapod.preferences.UserPreferences; @@ -15,10 +18,10 @@ public class AlarmUpdateReceiver extends BroadcastReceiver { public void onReceive(Context context, Intent intent) { if (BuildConfig.DEBUG) Log.d(TAG, "Received intent"); - if (intent.getAction().equals(Intent.ACTION_BOOT_COMPLETED)) { + if (StringUtils.equals(intent.getAction(), Intent.ACTION_BOOT_COMPLETED)) { if (BuildConfig.DEBUG) Log.d(TAG, "Resetting update alarm after reboot"); - } else if (intent.getAction().equals(Intent.ACTION_PACKAGE_REPLACED)) { + } else if (StringUtils.equals(intent.getAction(), Intent.ACTION_PACKAGE_REPLACED)) { if (BuildConfig.DEBUG) Log.d(TAG, "Resetting update alarm after app upgrade"); } diff --git a/src/de/danoeh/antennapod/receiver/ConnectivityActionReceiver.java b/src/de/danoeh/antennapod/receiver/ConnectivityActionReceiver.java index 31c053386..4dcf0b6aa 100644 --- a/src/de/danoeh/antennapod/receiver/ConnectivityActionReceiver.java +++ b/src/de/danoeh/antennapod/receiver/ConnectivityActionReceiver.java @@ -6,6 +6,9 @@ import android.content.Intent; import android.net.ConnectivityManager; import android.net.NetworkInfo; import android.util.Log; + +import org.apache.commons.lang3.StringUtils; + import de.danoeh.antennapod.BuildConfig; import de.danoeh.antennapod.storage.DBTasks; import de.danoeh.antennapod.storage.DownloadRequester; @@ -16,7 +19,7 @@ public class ConnectivityActionReceiver extends BroadcastReceiver { @Override public void onReceive(final Context context, Intent intent) { - if (intent.getAction().equals(ConnectivityManager.CONNECTIVITY_ACTION)) { + if (StringUtils.equals(intent.getAction(), ConnectivityManager.CONNECTIVITY_ACTION)) { if (BuildConfig.DEBUG) Log.d(TAG, "Received intent"); diff --git a/src/de/danoeh/antennapod/receiver/FeedUpdateReceiver.java b/src/de/danoeh/antennapod/receiver/FeedUpdateReceiver.java index a7482fbf4..3c283a30b 100644 --- a/src/de/danoeh/antennapod/receiver/FeedUpdateReceiver.java +++ b/src/de/danoeh/antennapod/receiver/FeedUpdateReceiver.java @@ -6,6 +6,9 @@ import android.content.Intent; import android.net.ConnectivityManager; import android.net.NetworkInfo; import android.util.Log; + +import org.apache.commons.lang3.StringUtils; + import de.danoeh.antennapod.BuildConfig; import de.danoeh.antennapod.preferences.UserPreferences; import de.danoeh.antennapod.storage.DBTasks; @@ -17,7 +20,7 @@ public class FeedUpdateReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { - if (intent.getAction().equals(ACTION_REFRESH_FEEDS)) { + if (StringUtils.equals(intent.getAction(), ACTION_REFRESH_FEEDS)) { if (BuildConfig.DEBUG) Log.d(TAG, "Received intent"); boolean mobileUpdate = UserPreferences.isAllowMobileUpdate(); diff --git a/src/de/danoeh/antennapod/receiver/PlayerWidget.java b/src/de/danoeh/antennapod/receiver/PlayerWidget.java index 5748ced3b..9f8892181 100644 --- a/src/de/danoeh/antennapod/receiver/PlayerWidget.java +++ b/src/de/danoeh/antennapod/receiver/PlayerWidget.java @@ -5,6 +5,9 @@ import android.appwidget.AppWidgetProvider; import android.content.Context; import android.content.Intent; import android.util.Log; + +import org.apache.commons.lang3.StringUtils; + import de.danoeh.antennapod.BuildConfig; import de.danoeh.antennapod.service.playback.PlayerWidgetService; @@ -15,9 +18,9 @@ public class PlayerWidget extends AppWidgetProvider { @Override public void onReceive(Context context, Intent intent) { - if (intent.getAction().equals(FORCE_WIDGET_UPDATE)) { + if (StringUtils.equals(intent.getAction(), FORCE_WIDGET_UPDATE)) { startUpdate(context); - } else if (intent.getAction().equals(STOP_WIDGET_UPDATE)) { + } else if (StringUtils.equals(intent.getAction(), STOP_WIDGET_UPDATE)) { stopUpdate(context); } diff --git a/src/de/danoeh/antennapod/service/download/DownloadRequest.java b/src/de/danoeh/antennapod/service/download/DownloadRequest.java index 7ff9bc01c..e803d30d4 100644 --- a/src/de/danoeh/antennapod/service/download/DownloadRequest.java +++ b/src/de/danoeh/antennapod/service/download/DownloadRequest.java @@ -3,6 +3,8 @@ package de.danoeh.antennapod.service.download; import android.os.Parcel; import android.os.Parcelable; +import org.apache.commons.lang3.Validate; + public class DownloadRequest implements Parcelable { private final String destination; @@ -21,15 +23,9 @@ public class DownloadRequest implements Parcelable { public DownloadRequest(String destination, String source, String title, long feedfileId, int feedfileType, String username, String password, boolean deleteOnFailure) { - if (destination == null) { - throw new IllegalArgumentException("Destination must not be null"); - } - if (source == null) { - throw new IllegalArgumentException("Source must not be null"); - } - if (title == null) { - throw new IllegalArgumentException("Title must not be null"); - } + Validate.notNull(destination); + Validate.notNull(source); + Validate.notNull(title); this.destination = destination; this.source = source; @@ -110,11 +106,14 @@ public class DownloadRequest implements Parcelable { if (size != that.size) return false; if (soFar != that.soFar) return false; if (statusMsg != that.statusMsg) return false; - if (destination != null ? !destination.equals(that.destination) : that.destination != null) return false; - if (password != null ? !password.equals(that.password) : that.password != null) return false; + if (destination != null ? !destination.equals(that.destination) : that.destination != null) + return false; + if (password != null ? !password.equals(that.password) : that.password != null) + return false; if (source != null ? !source.equals(that.source) : that.source != null) return false; if (title != null ? !title.equals(that.title) : that.title != null) return false; - if (username != null ? !username.equals(that.username) : that.username != null) return false; + if (username != null ? !username.equals(that.username) : that.username != null) + return false; return true; } diff --git a/src/de/danoeh/antennapod/service/download/DownloadService.java b/src/de/danoeh/antennapod/service/download/DownloadService.java index 4f60ef8d6..63be91b57 100644 --- a/src/de/danoeh/antennapod/service/download/DownloadService.java +++ b/src/de/danoeh/antennapod/service/download/DownloadService.java @@ -19,30 +19,63 @@ import android.os.IBinder; import android.support.v4.app.NotificationCompat; import android.util.Log; import android.webkit.URLUtil; + +import org.apache.commons.io.FileUtils; +import org.apache.commons.lang3.ArrayUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; +import org.apache.http.HttpStatus; +import org.xml.sax.SAXException; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.LinkedList; +import java.util.List; +import java.util.Queue; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.Callable; +import java.util.concurrent.CompletionService; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorCompletionService; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.LinkedBlockingDeque; +import java.util.concurrent.RejectedExecutionHandler; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import javax.xml.parsers.ParserConfigurationException; + import de.danoeh.antennapod.BuildConfig; import de.danoeh.antennapod.R; import de.danoeh.antennapod.activity.DownloadAuthenticationActivity; import de.danoeh.antennapod.activity.MainActivity; import de.danoeh.antennapod.adapter.NavListAdapter; -import de.danoeh.antennapod.feed.*; +import de.danoeh.antennapod.feed.EventDistributor; +import de.danoeh.antennapod.feed.Feed; +import de.danoeh.antennapod.feed.FeedImage; +import de.danoeh.antennapod.feed.FeedItem; +import de.danoeh.antennapod.feed.FeedMedia; +import de.danoeh.antennapod.feed.FeedPreferences; import de.danoeh.antennapod.fragment.DownloadsFragment; -import de.danoeh.antennapod.storage.*; +import de.danoeh.antennapod.storage.DBReader; +import de.danoeh.antennapod.storage.DBTasks; +import de.danoeh.antennapod.storage.DBWriter; +import de.danoeh.antennapod.storage.DownloadRequestException; +import de.danoeh.antennapod.storage.DownloadRequester; import de.danoeh.antennapod.syndication.handler.FeedHandler; import de.danoeh.antennapod.syndication.handler.UnsupportedFeedtypeException; import de.danoeh.antennapod.util.ChapterUtils; import de.danoeh.antennapod.util.DownloadError; import de.danoeh.antennapod.util.InvalidFeedException; -import org.apache.commons.io.FileUtils; -import org.apache.commons.lang3.StringUtils; -import org.apache.http.HttpStatus; -import org.xml.sax.SAXException; - -import javax.xml.parsers.ParserConfigurationException; -import java.io.File; -import java.io.IOException; -import java.util.*; -import java.util.concurrent.*; -import java.util.concurrent.atomic.AtomicInteger; /** * Manages the download of feedfiles in the app. Downloads can be enqueued viathe startService intent. @@ -82,9 +115,14 @@ public class DownloadService extends Service { public static final String EXTRA_REQUEST = "request"; /** - * Stores DownloadStatus objects of completed downloads for creating a report at the end of the lifecylce. + * Stores new media files that will be queued for auto-download if possible. + */ + private List<Long> newMediaFiles; + + /** + * Contains all completed downloads that have not been included in the report yet. */ - private List<DownloadStatus> completedDownloads; + private List<DownloadStatus> reportQueue; private ExecutorService syncExecutor; private CompletionService<Downloader> downloadExecutor; @@ -206,7 +244,8 @@ public class DownloadService extends Service { Log.d(TAG, "Service started"); isRunning = true; handler = new Handler(); - completedDownloads = Collections.synchronizedList(new ArrayList<DownloadStatus>()); + newMediaFiles = Collections.synchronizedList(new ArrayList<Long>()); + reportQueue = Collections.synchronizedList(new ArrayList<DownloadStatus>()); downloads = new ArrayList<Downloader>(); numberOfDownloads = new AtomicInteger(0); @@ -285,7 +324,10 @@ public class DownloadService extends Service { cancelNotificationUpdater(); unregisterReceiver(cancelDownloadReceiver); - DBTasks.autodownloadUndownloadedItems(getApplicationContext()); + if (!newMediaFiles.isEmpty()) { + DBTasks.autodownloadUndownloadedItems(getApplicationContext(), + ArrayUtils.toPrimitive(newMediaFiles.toArray(new Long[newMediaFiles.size()]))); + } } @SuppressLint("NewApi") @@ -394,12 +436,10 @@ public class DownloadService extends Service { @Override public void onReceive(Context context, Intent intent) { - if (intent.getAction().equals(ACTION_CANCEL_DOWNLOAD)) { + if (StringUtils.equals(intent.getAction(), ACTION_CANCEL_DOWNLOAD)) { String url = intent.getStringExtra(EXTRA_DOWNLOAD_URL); - if (url == null) { - throw new IllegalArgumentException( - "ACTION_CANCEL_DOWNLOAD intent needs download url extra"); - } + Validate.notNull(url, "ACTION_CANCEL_DOWNLOAD intent needs download url extra"); + if (BuildConfig.DEBUG) Log.d(TAG, "Cancelling download with url " + url); Downloader d = getDownloader(url); @@ -409,7 +449,7 @@ public class DownloadService extends Service { Log.e(TAG, "Could not cancel download with url " + url); } - } else if (intent.getAction().equals(ACTION_CANCEL_ALL_DOWNLOADS)) { + } else if (StringUtils.equals(intent.getAction(), ACTION_CANCEL_ALL_DOWNLOADS)) { for (Downloader d : downloads) { d.cancel(); if (BuildConfig.DEBUG) @@ -482,7 +522,7 @@ public class DownloadService extends Service { * @param status the download that is going to be saved */ private void saveDownloadStatus(DownloadStatus status) { - completedDownloads.add(status); + reportQueue.add(status); DBWriter.addDownloadStatus(this, status); } @@ -505,7 +545,7 @@ public class DownloadService extends Service { // a download report is created if at least one download has failed // (excluding failed image downloads) - for (DownloadStatus status : completedDownloads) { + for (DownloadStatus status : reportQueue) { if (status.isSuccessful()) { successfulDownloads++; } else if (!status.isCancelled()) { @@ -552,7 +592,7 @@ public class DownloadService extends Service { if (BuildConfig.DEBUG) Log.d(TAG, "No report is created"); } - completedDownloads.clear(); + reportQueue.clear(); } /** @@ -698,7 +738,8 @@ public class DownloadService extends Service { Log.d(TAG, "Waiting for " + (startTime + WAIT_TIMEOUT - currentTime) + " ms"); sleep(startTime + WAIT_TIMEOUT - currentTime); } catch (InterruptedException e) { - if (BuildConfig.DEBUG) Log.d(TAG, "interrupted while waiting for more downloads"); + if (BuildConfig.DEBUG) + Log.d(TAG, "interrupted while waiting for more downloads"); tasks += pollCompletedDownloads(); } finally { currentTime = System.currentTimeMillis(); @@ -794,6 +835,14 @@ public class DownloadService extends Service { ); } } + + // queue new media files for automatic download + for (FeedItem item : savedFeed.getItems()) { + if (!item.isRead() && item.hasMedia() && !item.getMedia().isDownloaded()) { + newMediaFiles.add(item.getMedia().getId()); + } + } + numberOfDownloads.decrementAndGet(); } @@ -1033,12 +1082,9 @@ public class DownloadService extends Service { private DownloadStatus status; public ImageHandlerThread(DownloadStatus status, DownloadRequest request) { - if (status == null) { - throw new IllegalArgumentException("Status must not be null"); - } - if (request == null) { - throw new IllegalArgumentException("Request must not be null"); - } + Validate.notNull(status); + Validate.notNull(request); + this.status = status; this.request = request; } @@ -1070,12 +1116,8 @@ public class DownloadService extends Service { private DownloadStatus status; public MediaHandlerThread(DownloadStatus status, DownloadRequest request) { - if (status == null) { - throw new IllegalArgumentException("Status must not be null"); - } - if (request == null) { - throw new IllegalArgumentException("Request must not be null"); - } + Validate.notNull(status); + Validate.notNull(request); this.status = status; this.request = request; diff --git a/src/de/danoeh/antennapod/service/download/DownloadStatus.java b/src/de/danoeh/antennapod/service/download/DownloadStatus.java index b240cddbc..1d76770bb 100644 --- a/src/de/danoeh/antennapod/service/download/DownloadStatus.java +++ b/src/de/danoeh/antennapod/service/download/DownloadStatus.java @@ -1,5 +1,7 @@ package de.danoeh.antennapod.service.download; +import org.apache.commons.lang3.Validate; + import de.danoeh.antennapod.feed.FeedFile; import de.danoeh.antennapod.util.DownloadError; @@ -59,9 +61,8 @@ public class DownloadStatus { public DownloadStatus(DownloadRequest request, DownloadError reason, boolean successful, boolean cancelled, String reasonDetailed) { - if (request == null) { - throw new IllegalArgumentException("request must not be null"); - } + Validate.notNull(request); + this.title = request.getTitle(); this.feedfileId = request.getFeedfileId(); this.feedfileType = request.getFeedfileType(); @@ -75,9 +76,7 @@ public class DownloadStatus { /** Constructor for creating new completed downloads. */ public DownloadStatus(FeedFile feedfile, String title, DownloadError reason, boolean successful, String reasonDetailed) { - if (feedfile == null) { - throw new IllegalArgumentException("feedfile must not be null"); - } + Validate.notNull(feedfile); this.title = title; this.done = true; diff --git a/src/de/danoeh/antennapod/service/playback/PlaybackService.java b/src/de/danoeh/antennapod/service/playback/PlaybackService.java index b7ff62129..163a57ed2 100644 --- a/src/de/danoeh/antennapod/service/playback/PlaybackService.java +++ b/src/de/danoeh/antennapod/service/playback/PlaybackService.java @@ -22,6 +22,10 @@ import android.util.Log; import android.util.Pair; import android.view.KeyEvent; import android.view.SurfaceHolder; +import android.widget.Toast; + +import org.apache.commons.lang3.StringUtils; + import de.danoeh.antennapod.BuildConfig; import de.danoeh.antennapod.PodcastApp; import de.danoeh.antennapod.R; @@ -39,9 +43,9 @@ import de.danoeh.antennapod.storage.DBTasks; import de.danoeh.antennapod.storage.DBWriter; import de.danoeh.antennapod.util.BitmapDecoder; import de.danoeh.antennapod.util.QueueAccess; +import de.danoeh.antennapod.util.flattr.FlattrThing; import de.danoeh.antennapod.util.flattr.FlattrUtils; import de.danoeh.antennapod.util.playback.Playable; -import de.danoeh.antennapod.util.playback.PlaybackController; import java.util.List; @@ -282,7 +286,8 @@ public class PlaybackService extends Service { if (BuildConfig.DEBUG) Log.d(TAG, "Handling keycode: " + keycode); - final PlayerStatus status = mediaPlayer.getPSMPInfo().playerStatus; + final PlaybackServiceMediaPlayer.PSMPInfo info = mediaPlayer.getPSMPInfo(); + final PlayerStatus status = info.playerStatus; switch (keycode) { case KeyEvent.KEYCODE_HEADSETHOOK: case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE: @@ -310,14 +315,20 @@ public class PlaybackService extends Service { mediaPlayer.pause(true, true); } break; - case KeyEvent.KEYCODE_MEDIA_FAST_FORWARD: { - mediaPlayer.seekDelta(PlaybackController.DEFAULT_SEEK_DELTA); + case KeyEvent.KEYCODE_MEDIA_NEXT: + case KeyEvent.KEYCODE_MEDIA_FAST_FORWARD: + mediaPlayer.seekDelta(UserPreferences.getSeekDeltaMs()); break; - } - case KeyEvent.KEYCODE_MEDIA_REWIND: { - mediaPlayer.seekDelta(-PlaybackController.DEFAULT_SEEK_DELTA); + case KeyEvent.KEYCODE_MEDIA_PREVIOUS: + case KeyEvent.KEYCODE_MEDIA_REWIND: + mediaPlayer.seekDelta(-UserPreferences.getSeekDeltaMs()); + break; + default: + if (info.playable != null && info.playerStatus == PlayerStatus.PLAYING) { // only notify the user about an unknown key event if it is actually doing something + String message = String.format(getResources().getString(R.string.unknown_media_key), keycode); + Toast.makeText(this, message, Toast.LENGTH_SHORT).show(); + } break; - } } } @@ -502,6 +513,11 @@ public class PlaybackService extends Service { DBWriter.removeQueueItem(PlaybackService.this, item.getId(), true); } DBWriter.addItemToPlaybackHistory(PlaybackService.this, (FeedMedia) media); + + // auto-flattr if enabled + if (isAutoFlattrable(media) && UserPreferences.getAutoFlattrPlayedDurationThreshold() == 1.0f) { + DBTasks.flattrItemIfLoggedIn(PlaybackService.this, item); + } } // Load next episode if previous episode was in the queue and if there @@ -752,14 +768,13 @@ public class PlaybackService extends Service { FeedItem item = m.getItem(); m.setPlayedDuration(m.getPlayedDuration() + ((int)(deltaPlayedDuration * playbackSpeed))); // Auto flattr - if (FlattrUtils.hasToken() && UserPreferences.isAutoFlattr() && item.getPaymentLink() != null && item.getFlattrStatus().getUnflattred() && - (m.getPlayedDuration() > UserPreferences.getPlayedDurationAutoflattrThreshold() * duration)) { + if (isAutoFlattrable(m) && + (m.getPlayedDuration() > UserPreferences.getAutoFlattrPlayedDurationThreshold() * duration)) { if (BuildConfig.DEBUG) Log.d(TAG, "saveCurrentPosition: performing auto flattr since played duration " + Integer.toString(m.getPlayedDuration()) - + " is " + UserPreferences.getPlayedDurationAutoflattrThreshold() * 100 + "% of file duration " + Integer.toString(duration)); - item.getFlattrStatus().setFlattrQueue(); - DBWriter.setFeedItemFlattrStatus(PodcastApp.getInstance(), item, false); + + " is " + UserPreferences.getAutoFlattrPlayedDurationThreshold() * 100 + "% of file duration " + Integer.toString(duration)); + DBTasks.flattrItemIfLoggedIn(this, item); } } playable.saveCurrentPosition(PreferenceManager @@ -889,8 +904,7 @@ public class PlaybackService extends Service { @Override public void onReceive(Context context, Intent intent) { - if (intent.getAction() != null && - intent.getAction().equals(Intent.ACTION_HEADSET_PLUG)) { + if (StringUtils.equals(intent.getAction(), Intent.ACTION_HEADSET_PLUG)) { int state = intent.getIntExtra("state", -1); if (state != -1) { if (BuildConfig.DEBUG) @@ -932,8 +946,7 @@ public class PlaybackService extends Service { @Override public void onReceive(Context context, Intent intent) { - if (intent.getAction() != null && - intent.getAction().equals(ACTION_SHUTDOWN_PLAYBACK_SERVICE)) { + if (StringUtils.equals(intent.getAction(), ACTION_SHUTDOWN_PLAYBACK_SERVICE)) { stopSelf(); } } @@ -943,8 +956,7 @@ public class PlaybackService extends Service { private BroadcastReceiver skipCurrentEpisodeReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { - if (intent.getAction() != null && - intent.getAction().equals(ACTION_SKIP_CURRENT_EPISODE)) { + if (StringUtils.equals(intent.getAction(), ACTION_SKIP_CURRENT_EPISODE)) { if (BuildConfig.DEBUG) Log.d(TAG, "Received SKIP_CURRENT_EPISODE intent"); mediaPlayer.endPlayback(); @@ -1045,4 +1057,13 @@ public class PlaybackService extends Service { return mediaPlayer.getVideoSize(); } + private boolean isAutoFlattrable(Playable p) { + if (p != null && p instanceof FeedMedia) { + FeedMedia media = (FeedMedia) p; + FeedItem item = ((FeedMedia) p).getItem(); + return item != null && FlattrUtils.hasToken() && UserPreferences.isAutoFlattr() && item.getPaymentLink() != null && item.getFlattrStatus().getUnflattred(); + } else { + return false; + } + } } diff --git a/src/de/danoeh/antennapod/service/playback/PlaybackServiceMediaPlayer.java b/src/de/danoeh/antennapod/service/playback/PlaybackServiceMediaPlayer.java index 2915da5a1..477eea9a6 100644 --- a/src/de/danoeh/antennapod/service/playback/PlaybackServiceMediaPlayer.java +++ b/src/de/danoeh/antennapod/service/playback/PlaybackServiceMediaPlayer.java @@ -7,6 +7,9 @@ import android.media.RemoteControlClient; import android.util.Log; import android.util.Pair; import android.view.SurfaceHolder; + +import org.apache.commons.lang3.Validate; + import de.danoeh.antennapod.BuildConfig; import de.danoeh.antennapod.feed.Chapter; import de.danoeh.antennapod.feed.MediaType; @@ -61,10 +64,8 @@ public class PlaybackServiceMediaPlayer { private final ThreadPoolExecutor executor; public PlaybackServiceMediaPlayer(Context context, PSMPCallback callback) { - if (context == null) - throw new IllegalArgumentException("context = null"); - if (callback == null) - throw new IllegalArgumentException("callback = null"); + Validate.notNull(context); + Validate.notNull(callback); this.context = context; this.callback = callback; @@ -115,8 +116,8 @@ public class PlaybackServiceMediaPlayer { * @param prepareImmediately Set to true if the method should also prepare the episode for playback. */ public void playMediaObject(final Playable playable, final boolean stream, final boolean startWhenPrepared, final boolean prepareImmediately) { - if (playable == null) - throw new IllegalArgumentException("playable = null"); + Validate.notNull(playable); + if (BuildConfig.DEBUG) Log.d(TAG, "Play media object."); executor.submit(new Runnable() { @Override @@ -125,6 +126,7 @@ public class PlaybackServiceMediaPlayer { try { playMediaObject(playable, false, stream, startWhenPrepared, prepareImmediately); } catch (RuntimeException e) { + e.printStackTrace(); throw e; } finally { playerLock.unlock(); @@ -142,8 +144,7 @@ public class PlaybackServiceMediaPlayer { * @see #playMediaObject(de.danoeh.antennapod.util.playback.Playable, boolean, boolean, boolean) */ private void playMediaObject(final Playable playable, final boolean forceReset, final boolean stream, final boolean startWhenPrepared, final boolean prepareImmediately) { - if (playable == null) - throw new IllegalArgumentException("playable = null"); + Validate.notNull(playable); if (!playerLock.isHeldByCurrentThread()) throw new IllegalStateException("method requires playerLock"); @@ -458,8 +459,8 @@ public class PlaybackServiceMediaPlayer { * Seek to the start of the specified chapter. */ public void seekToChapter(Chapter c) { - if (c == null) - throw new IllegalArgumentException("c = null"); + Validate.notNull(c); + seekTo((int) c.getStart()); } @@ -662,8 +663,8 @@ public class PlaybackServiceMediaPlayer { * @param newMedia The new playable object of the PSMP object. This can be null. */ private synchronized void setPlayerStatus(PlayerStatus newStatus, Playable newMedia) { - if (newStatus == null) - throw new IllegalArgumentException("newStatus = null"); + Validate.notNull(newStatus); + if (BuildConfig.DEBUG) Log.d(TAG, "Setting player status to " + newStatus); this.playerStatus = newStatus; diff --git a/src/de/danoeh/antennapod/service/playback/PlaybackServiceTaskManager.java b/src/de/danoeh/antennapod/service/playback/PlaybackServiceTaskManager.java index 3ab06910a..680ec2e2f 100644 --- a/src/de/danoeh/antennapod/service/playback/PlaybackServiceTaskManager.java +++ b/src/de/danoeh/antennapod/service/playback/PlaybackServiceTaskManager.java @@ -2,6 +2,9 @@ package de.danoeh.antennapod.service.playback; import android.content.Context; import android.util.Log; + +import org.apache.commons.lang3.Validate; + import de.danoeh.antennapod.BuildConfig; import de.danoeh.antennapod.feed.EventDistributor; import de.danoeh.antennapod.feed.FeedItem; @@ -52,10 +55,8 @@ public class PlaybackServiceTaskManager { * @param callback A PSTMCallback object for notifying the user about updates. Must not be null. */ public PlaybackServiceTaskManager(Context context, PSTMCallback callback) { - if (context == null) - throw new IllegalArgumentException("context must not be null"); - if (callback == null) - throw new IllegalArgumentException("callback must not be null"); + Validate.notNull(context); + Validate.notNull(callback); this.context = context; this.callback = callback; @@ -195,8 +196,7 @@ public class PlaybackServiceTaskManager { * @throws java.lang.IllegalArgumentException if waitingTime <= 0 */ public synchronized void setSleepTimer(long waitingTime) { - if (waitingTime <= 0) - throw new IllegalArgumentException("waitingTime <= 0"); + Validate.isTrue(waitingTime > 0, "Waiting time <= 0"); if (BuildConfig.DEBUG) Log.d(TAG, "Setting sleep timer to " + Long.toString(waitingTime) @@ -271,8 +271,7 @@ public class PlaybackServiceTaskManager { * On completion, the callback's onChapterLoaded method will be called. */ public synchronized void startChapterLoader(final Playable media) { - if (media == null) - throw new IllegalArgumentException("media = null"); + Validate.notNull(media); if (isChapterLoaderActive()) { cancelChapterLoader(); diff --git a/src/de/danoeh/antennapod/spa/SPAUtil.java b/src/de/danoeh/antennapod/spa/SPAUtil.java index 0d83741ed..75cbd8b5a 100644 --- a/src/de/danoeh/antennapod/spa/SPAUtil.java +++ b/src/de/danoeh/antennapod/spa/SPAUtil.java @@ -5,6 +5,9 @@ import android.content.Intent; import android.content.SharedPreferences; import android.preference.PreferenceManager; import android.util.Log; + +import org.apache.commons.lang3.Validate; + import de.danoeh.antennapod.BuildConfig; import de.danoeh.antennapod.receiver.SPAReceiver; @@ -56,7 +59,7 @@ public class SPAUtil { */ public static void resetSPAPreferences(Context c) { if (BuildConfig.DEBUG) { - if (c == null) throw new IllegalArgumentException("c = null"); + Validate.notNull(c); SharedPreferences.Editor editor = PreferenceManager .getDefaultSharedPreferences(c.getApplicationContext()).edit(); editor.putBoolean(PREF_HAS_QUERIED_SP_APPS, false); diff --git a/src/de/danoeh/antennapod/storage/DBTasks.java b/src/de/danoeh/antennapod/storage/DBTasks.java index c6a34aeea..8d0ffd9c1 100644 --- a/src/de/danoeh/antennapod/storage/DBTasks.java +++ b/src/de/danoeh/antennapod/storage/DBTasks.java @@ -156,7 +156,7 @@ public final class DBTasks { if (FlattrUtils.hasToken()) { if (BuildConfig.DEBUG) Log.d(TAG, "Flattring all pending things."); - new FlattrClickWorker(context, FlattrClickWorker.FLATTR_NOTIFICATION).executeAsync(); // flattr pending things + new FlattrClickWorker(context).executeAsync(); // flattr pending things if (BuildConfig.DEBUG) Log.d(TAG, "Fetching flattr status."); new FlattrStatusFetcher(context).start(); @@ -386,9 +386,11 @@ public final class DBTasks { * This method is executed on an internal single thread executor. * * @param context Used for accessing the DB. + * @param mediaIds If this list is not empty, the method will only download a candidate for automatic downloading if + * its media ID is in the mediaIds list. * @return A Future that can be used for waiting for the methods completion. */ - public static Future<?> autodownloadUndownloadedItems(final Context context) { + public static Future<?> autodownloadUndownloadedItems(final Context context, final long... mediaIds) { return autodownloadExec.submit(new Runnable() { @Override public void run() { @@ -417,11 +419,17 @@ public final class DBTasks { - (downloadedEpisodes - deletedEpisodes); } + Arrays.sort(mediaIds); // sort for binary search + final boolean ignoreMediaIds = mediaIds.length == 0; List<FeedItem> itemsToDownload = new ArrayList<FeedItem>(); + if (episodeSpaceLeft > 0 && undownloadedEpisodes > 0) { for (int i = 0; i < queue.size(); i++) { // ignore playing item FeedItem item = queue.get(i); - if (item.hasMedia() && !item.getMedia().isDownloaded() + long mediaId = (item.hasMedia()) ? item.getMedia().getId() : -1; + if ((ignoreMediaIds || Arrays.binarySearch(mediaIds, mediaId) >= 0) + && item.hasMedia() + && !item.getMedia().isDownloaded() && !item.getMedia().isPlaying() && item.getFeed().getPreferences().getAutoDownload()) { itemsToDownload.add(item); @@ -433,9 +441,13 @@ public final class DBTasks { } } } + if (episodeSpaceLeft > 0 && undownloadedEpisodes > 0) { for (FeedItem item : unreadItems) { - if (item.hasMedia() && !item.getMedia().isDownloaded() + long mediaId = (item.hasMedia()) ? item.getMedia().getId() : -1; + if ((ignoreMediaIds || Arrays.binarySearch(mediaIds, mediaId) >= 0) + && item.hasMedia() + && !item.getMedia().isDownloaded() && item.getFeed().getPreferences().getAutoDownload()) { itemsToDownload.add(item); episodeSpaceLeft--; diff --git a/src/de/danoeh/antennapod/storage/DBWriter.java b/src/de/danoeh/antennapod/storage/DBWriter.java index ffdfc65fd..9916ac97f 100644 --- a/src/de/danoeh/antennapod/storage/DBWriter.java +++ b/src/de/danoeh/antennapod/storage/DBWriter.java @@ -868,7 +868,7 @@ public class DBWriter { adapter.setFeedItemFlattrStatus(item); adapter.close(); if (startFlattrClickWorker) { - new FlattrClickWorker(context, FlattrClickWorker.FLATTR_TOAST).executeAsync(); + new FlattrClickWorker(context).executeAsync(); } } }); @@ -891,7 +891,7 @@ public class DBWriter { adapter.setFeedFlattrStatus(feed); adapter.close(); if (startFlattrClickWorker) { - new FlattrClickWorker(context, FlattrClickWorker.FLATTR_TOAST).executeAsync(); + new FlattrClickWorker(context).executeAsync(); } } }); diff --git a/src/de/danoeh/antennapod/storage/DownloadRequester.java b/src/de/danoeh/antennapod/storage/DownloadRequester.java index 7bf21352a..d305c572b 100644 --- a/src/de/danoeh/antennapod/storage/DownloadRequester.java +++ b/src/de/danoeh/antennapod/storage/DownloadRequester.java @@ -14,6 +14,7 @@ import de.danoeh.antennapod.util.URLChecker; import org.apache.commons.io.FilenameUtils; import org.apache.commons.lang3.StringEscapeUtils; import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; import java.io.File; import java.util.Map; @@ -57,8 +58,9 @@ public class DownloadRequester { * @return True if the download request was accepted, false otherwise. */ public boolean download(Context context, DownloadRequest request) { - if (context == null) throw new IllegalArgumentException("context = null"); - if (request == null) throw new IllegalArgumentException("request = null"); + Validate.notNull(context); + Validate.notNull(request); + if (downloads.containsKey(request.getSource())) { if (BuildConfig.DEBUG) Log.i(TAG, "DownloadRequest is already stored."); return false; diff --git a/src/de/danoeh/antennapod/storage/PodDBAdapter.java b/src/de/danoeh/antennapod/storage/PodDBAdapter.java index 06c8b1fc9..671ac30d5 100644 --- a/src/de/danoeh/antennapod/storage/PodDBAdapter.java +++ b/src/de/danoeh/antennapod/storage/PodDBAdapter.java @@ -10,14 +10,23 @@ import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteDatabase.CursorFactory; import android.database.sqlite.SQLiteOpenHelper; import android.util.Log; -import de.danoeh.antennapod.BuildConfig; -import de.danoeh.antennapod.feed.*; -import de.danoeh.antennapod.service.download.DownloadStatus; -import de.danoeh.antennapod.util.flattr.FlattrStatus; + +import org.apache.commons.lang3.Validate; import java.util.Arrays; import java.util.List; +import de.danoeh.antennapod.BuildConfig; +import de.danoeh.antennapod.feed.Chapter; +import de.danoeh.antennapod.feed.Feed; +import de.danoeh.antennapod.feed.FeedComponent; +import de.danoeh.antennapod.feed.FeedImage; +import de.danoeh.antennapod.feed.FeedItem; +import de.danoeh.antennapod.feed.FeedMedia; +import de.danoeh.antennapod.feed.FeedPreferences; +import de.danoeh.antennapod.service.download.DownloadStatus; +import de.danoeh.antennapod.util.flattr.FlattrStatus; + // TODO Remove media column from feeditem table /** @@ -1024,9 +1033,8 @@ public class PodDBAdapter { * @throws IllegalArgumentException if limit < 0 */ public final Cursor getCompletedMediaCursor(int limit) { - if (limit < 0) { - throw new IllegalArgumentException("Limit must be >= 0"); - } + Validate.isTrue(limit >= 0, "Limit must be >= 0"); + Cursor c = db.query(TABLE_NAME_FEED_MEDIA, null, KEY_PLAYBACK_COMPLETION_DATE + " > 0 LIMIT " + limit, null, null, null, null); diff --git a/src/de/danoeh/antennapod/syndication/handler/TypeGetter.java b/src/de/danoeh/antennapod/syndication/handler/TypeGetter.java index 5ed9ff2b0..2496e112a 100644 --- a/src/de/danoeh/antennapod/syndication/handler/TypeGetter.java +++ b/src/de/danoeh/antennapod/syndication/handler/TypeGetter.java @@ -4,6 +4,8 @@ import android.util.Log; import de.danoeh.antennapod.BuildConfig; import de.danoeh.antennapod.feed.Feed; import org.apache.commons.io.input.XmlStreamReader; +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; import org.xmlpull.v1.XmlPullParserFactory; @@ -64,7 +66,7 @@ public class TypeGetter { } else { if (BuildConfig.DEBUG) Log.d(TAG, "Type is invalid"); - throw new UnsupportedFeedtypeException(Type.INVALID); + throw new UnsupportedFeedtypeException(Type.INVALID, tag); } } else { eventType = xpp.next(); @@ -73,7 +75,19 @@ public class TypeGetter { } catch (XmlPullParserException e) { e.printStackTrace(); - } catch (IOException e) { + // XML document might actually be a HTML document -> try to parse as HTML + String rootElement = null; + try { + if (Jsoup.parse(new File(feed.getFile_url()), null) != null) { + rootElement = "html"; + } + } catch (IOException e1) { + e1.printStackTrace(); + } finally { + throw new UnsupportedFeedtypeException(Type.INVALID, rootElement); + } + + } catch (IOException e) { e.printStackTrace(); } } diff --git a/src/de/danoeh/antennapod/syndication/handler/UnsupportedFeedtypeException.java b/src/de/danoeh/antennapod/syndication/handler/UnsupportedFeedtypeException.java index 67fbc9cc9..605dad2fb 100644 --- a/src/de/danoeh/antennapod/syndication/handler/UnsupportedFeedtypeException.java +++ b/src/de/danoeh/antennapod/syndication/handler/UnsupportedFeedtypeException.java @@ -5,18 +5,27 @@ import de.danoeh.antennapod.syndication.handler.TypeGetter.Type; public class UnsupportedFeedtypeException extends Exception { private static final long serialVersionUID = 9105878964928170669L; private TypeGetter.Type type; + private String rootElement; public UnsupportedFeedtypeException(Type type) { super(); this.type = type; - } - - public TypeGetter.Type getType() { + + public UnsupportedFeedtypeException(Type type, String rootElement) { + this.type = type; + this.rootElement = rootElement; + } + + public TypeGetter.Type getType() { return type; } - - @Override + + public String getRootElement() { + return rootElement; + } + + @Override public String getMessage() { if (type == TypeGetter.Type.INVALID) { return "Invalid type"; diff --git a/src/de/danoeh/antennapod/util/Converter.java b/src/de/danoeh/antennapod/util/Converter.java index 46a0d30b4..f4c2b2f66 100644 --- a/src/de/danoeh/antennapod/util/Converter.java +++ b/src/de/danoeh/antennapod/util/Converter.java @@ -78,5 +78,26 @@ public final class Converter { return String.format("%02d:%02d", h, m); } + + /** Converts long duration string (HH:MM:SS) to milliseconds. */ + public static int durationStringLongToMs(String input) { + String[] parts = input.split(":"); + if (parts.length != 3) { + return 0; + } + return Integer.valueOf(parts[0]) * 3600 * 1000 + + Integer.valueOf(parts[1]) * 60 * 1000 + + Integer.valueOf(parts[2]) * 1000; + } + + /** Converts short duration string (HH:MM) to milliseconds. */ + public static int durationStringShortToMs(String input) { + String[] parts = input.split(":"); + if (parts.length != 2) { + return 0; + } + return Integer.valueOf(parts[0]) * 3600 * 1000 + + Integer.valueOf(parts[1]) * 1000 * 60; + } } diff --git a/src/de/danoeh/antennapod/util/DuckType.java b/src/de/danoeh/antennapod/util/DuckType.java index 0dfc01508..163110418 100644 --- a/src/de/danoeh/antennapod/util/DuckType.java +++ b/src/de/danoeh/antennapod/util/DuckType.java @@ -6,6 +6,8 @@ import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; +import de.danoeh.antennapod.BuildConfig; + /** * Allows "duck typing" or dynamic invocation based on method signature rather * than type hierarchy. In other words, rather than checking whether something @@ -63,7 +65,7 @@ public class DuckType { */ @SuppressWarnings({ "rawtypes", "unchecked" }) public <T> T to(Class iface) { - assert iface.isInterface() : "cannot coerce object to a class, must be an interface"; + if (BuildConfig.DEBUG && !iface.isInterface()) throw new AssertionError("cannot coerce object to a class, must be an interface"); if (isA(iface)) { return (T) iface.cast(objectToCoerce); } diff --git a/src/de/danoeh/antennapod/util/URLChecker.java b/src/de/danoeh/antennapod/util/URLChecker.java index eb522ffa8..9997daaf7 100644 --- a/src/de/danoeh/antennapod/util/URLChecker.java +++ b/src/de/danoeh/antennapod/util/URLChecker.java @@ -1,6 +1,9 @@ package de.danoeh.antennapod.util; import android.util.Log; + +import org.apache.commons.lang3.StringUtils; + import de.danoeh.antennapod.BuildConfig; /** @@ -27,12 +30,16 @@ public final class URLChecker { */ public static String prepareURL(String url) { StringBuilder builder = new StringBuilder(); + url = StringUtils.trim(url); if (url.startsWith("feed://")) { if (BuildConfig.DEBUG) Log.d(TAG, "Replacing feed:// with http://"); url = url.replaceFirst("feed://", "http://"); } else if (url.startsWith("pcast://")) { if (BuildConfig.DEBUG) Log.d(TAG, "Replacing pcast:// with http://"); url = url.replaceFirst("pcast://", "http://"); + } else if (url.startsWith("itpc")) { + if (BuildConfig.DEBUG) Log.d(TAG, "Replacing itpc:// with http://"); + url = url.replaceFirst("itpc://", "http://"); } else if (!(url.startsWith("http://") || url.startsWith("https://"))) { if (BuildConfig.DEBUG) Log.d(TAG, "Adding http:// at the beginning of the URL"); builder.append("http://"); diff --git a/src/de/danoeh/antennapod/util/menuhandler/FeedItemMenuHandler.java b/src/de/danoeh/antennapod/util/menuhandler/FeedItemMenuHandler.java index 6733430da..2c7a7f074 100644 --- a/src/de/danoeh/antennapod/util/menuhandler/FeedItemMenuHandler.java +++ b/src/de/danoeh/antennapod/util/menuhandler/FeedItemMenuHandler.java @@ -3,6 +3,7 @@ package de.danoeh.antennapod.util.menuhandler; import android.content.Context; import android.content.Intent; import android.net.Uri; + import de.danoeh.antennapod.BuildConfig; import de.danoeh.antennapod.R; import de.danoeh.antennapod.feed.FeedItem; @@ -113,6 +114,25 @@ public class FeedItemMenuHandler { return true; } + /** + * The same method as onPrepareMenu(MenuInterface, FeedItem, boolean, QueueAccess), but lets the + * caller also specify a list of menu items that should not be shown. + * + * @param excludeIds Menu item that should be excluded + * @return true if selectedItem is not null. + */ + public static boolean onPrepareMenu(MenuInterface mi, + FeedItem selectedItem, boolean showExtendedMenu, QueueAccess queueAccess, int... excludeIds) { + boolean rc = onPrepareMenu(mi, selectedItem, showExtendedMenu, queueAccess); + if (rc && excludeIds != null) { + for (int id : excludeIds) { + mi.setItemVisibility(id, false); + } + } + + return rc; + } + public static boolean onMenuItemClicked(Context context, int menuItemId, FeedItem selectedItem) throws DownloadRequestException { DownloadRequester requester = DownloadRequester.getInstance(); diff --git a/src/de/danoeh/antennapod/util/menuhandler/MenuItemUtils.java b/src/de/danoeh/antennapod/util/menuhandler/MenuItemUtils.java index e75fa394a..7aa04d24c 100644 --- a/src/de/danoeh/antennapod/util/menuhandler/MenuItemUtils.java +++ b/src/de/danoeh/antennapod/util/menuhandler/MenuItemUtils.java @@ -4,6 +4,7 @@ import android.support.v4.view.MenuItemCompat; import android.support.v7.widget.SearchView; import android.view.Menu; import android.view.MenuItem; + import de.danoeh.antennapod.R; /** @@ -17,4 +18,14 @@ public class MenuItemUtils { MenuItemCompat.setActionView(item, searchView); return item; } + + /** + * Checks if the navigation drawer of the DrawerActivity is opened. This can be useful for Fragments + * that hide their menu if the navigation drawer is open. + * + * @return True if the drawer is open, false otherwise (also if the parameter is null) + */ + public static boolean isActivityDrawerOpen(NavDrawerActivity activity) { + return activity != null && activity.isDrawerOpen(); + } } diff --git a/src/de/danoeh/antennapod/util/menuhandler/NavDrawerActivity.java b/src/de/danoeh/antennapod/util/menuhandler/NavDrawerActivity.java new file mode 100644 index 000000000..9c611a452 --- /dev/null +++ b/src/de/danoeh/antennapod/util/menuhandler/NavDrawerActivity.java @@ -0,0 +1,9 @@ +package de.danoeh.antennapod.util.menuhandler; + +/** + * Defines useful methods for activities that have a navigation drawer + */ +public interface NavDrawerActivity { + + public boolean isDrawerOpen(); +} diff --git a/src/de/danoeh/antennapod/util/playback/Playable.java b/src/de/danoeh/antennapod/util/playback/Playable.java index 8eefb0be5..9ed45abfc 100644 --- a/src/de/danoeh/antennapod/util/playback/Playable.java +++ b/src/de/danoeh/antennapod/util/playback/Playable.java @@ -12,6 +12,7 @@ import de.danoeh.antennapod.feed.MediaType; import de.danoeh.antennapod.storage.DBReader; import de.danoeh.antennapod.util.ShownotesProvider; import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.Validate; import java.io.ByteArrayInputStream; import java.io.InputStream; @@ -216,9 +217,8 @@ public interface Playable extends Parcelable, private Playable playable; public DefaultPlayableImageLoader(Playable playable) { - if (playable == null) { - throw new IllegalArgumentException("Playable must not be null"); - } + Validate.notNull(playable); + this.playable = playable; } diff --git a/src/de/danoeh/antennapod/util/playback/PlaybackController.java b/src/de/danoeh/antennapod/util/playback/PlaybackController.java index 1992fce2c..64dbf4868 100644 --- a/src/de/danoeh/antennapod/util/playback/PlaybackController.java +++ b/src/de/danoeh/antennapod/util/playback/PlaybackController.java @@ -15,12 +15,17 @@ import android.view.View.OnClickListener; import android.widget.ImageButton; import android.widget.SeekBar; import android.widget.TextView; + +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; + import de.danoeh.antennapod.BuildConfig; import de.danoeh.antennapod.R; import de.danoeh.antennapod.feed.Chapter; import de.danoeh.antennapod.feed.FeedMedia; import de.danoeh.antennapod.feed.MediaType; import de.danoeh.antennapod.preferences.PlaybackPreferences; +import de.danoeh.antennapod.preferences.UserPreferences; import de.danoeh.antennapod.service.playback.PlaybackService; import de.danoeh.antennapod.service.playback.PlaybackServiceMediaPlayer; import de.danoeh.antennapod.service.playback.PlayerStatus; @@ -37,7 +42,6 @@ import java.util.concurrent.*; public abstract class PlaybackController { private static final String TAG = "PlaybackController"; - public static final int DEFAULT_SEEK_DELTA = 30000; public static final int INVALID_TIME = -1; private final Activity activity; @@ -62,8 +66,8 @@ public abstract class PlaybackController { private boolean reinitOnPause; public PlaybackController(Activity activity, boolean reinitOnPause) { - if (activity == null) - throw new IllegalArgumentException("activity = null"); + Validate.notNull(activity); + this.activity = activity; this.reinitOnPause = reinitOnPause; schedExecutor = new ScheduledThreadPoolExecutor(SCHED_EX_POOLSIZE, @@ -360,7 +364,7 @@ public abstract class PlaybackController { @Override public void onReceive(Context context, Intent intent) { if (isConnectedToPlaybackService()) { - if (intent.getAction().equals( + if (StringUtils.equals(intent.getAction(), PlaybackService.ACTION_SHUTDOWN_PLAYBACK_SERVICE)) { release(); onShutdownNotification(); @@ -605,7 +609,7 @@ public abstract class PlaybackController { @Override public void onClick(View v) { if (status == PlayerStatus.PLAYING) { - playbackService.seekDelta(-DEFAULT_SEEK_DELTA); + playbackService.seekDelta(-UserPreferences.getSeekDeltaMs()); } } }; @@ -616,7 +620,7 @@ public abstract class PlaybackController { @Override public void onClick(View v) { if (status == PlayerStatus.PLAYING) { - playbackService.seekDelta(DEFAULT_SEEK_DELTA); + playbackService.seekDelta(UserPreferences.getSeekDeltaMs()); } } }; @@ -680,6 +684,12 @@ public abstract class PlaybackController { } } + public void seekTo(int time) { + if (playbackService != null) { + playbackService.seekTo(time); + } + } + public void setVideoSurface(SurfaceHolder holder) { if (playbackService != null) { playbackService.setVideoSurface(holder); diff --git a/src/de/danoeh/antennapod/util/playback/Timeline.java b/src/de/danoeh/antennapod/util/playback/Timeline.java new file mode 100644 index 000000000..ceed68183 --- /dev/null +++ b/src/de/danoeh/antennapod/util/playback/Timeline.java @@ -0,0 +1,161 @@ +package de.danoeh.antennapod.util.playback; + +import android.content.Context; +import android.content.res.TypedArray; +import android.util.Log; +import android.util.TypedValue; + +import org.apache.commons.lang3.Validate; +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; +import org.jsoup.nodes.Element; +import org.jsoup.select.Elements; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import de.danoeh.antennapod.BuildConfig; +import de.danoeh.antennapod.util.Converter; +import de.danoeh.antennapod.util.ShownotesProvider; + +/** + * Connects chapter information and shownotes of a shownotesProvider, for example by making it possible to use the + * shownotes to navigate to another position in the podcast or by highlighting certain parts of the shownotesProvider's + * shownotes. + * <p/> + * A timeline object needs a shownotesProvider from which the chapter information is retrieved and shownotes are generated. + */ +public class Timeline { + private static final String TAG = "Timeline"; + + private static final String WEBVIEW_STYLE = "@font-face { font-family: 'Roboto-Light'; src: url('file:///android_asset/Roboto-Light.ttf'); } * { color: %s; font-family: roboto-Light; font-size: 11pt; } a { font-style: normal; text-decoration: none; font-weight: normal; color: #00A8DF; } a.timecode { color: #669900; } img { display: block; margin: 10 auto; max-width: %s; height: auto; } body { margin: %dpx %dpx %dpx %dpx; }"; + + + private ShownotesProvider shownotesProvider; + + + private final String colorString; + private final int pageMargin; + + public Timeline(Context context, ShownotesProvider shownotesProvider) { + if (shownotesProvider == null) throw new IllegalArgumentException("shownotesProvider = null"); + this.shownotesProvider = shownotesProvider; + + TypedArray res = context + .getTheme() + .obtainStyledAttributes( + new int[]{android.R.attr.textColorPrimary}); + int colorResource = res.getColor(0, 0); + colorString = String.format("#%06X", + 0xFFFFFF & colorResource); + res.recycle(); + + pageMargin = (int) TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, 8, context.getResources() + .getDisplayMetrics() + ); + } + + private static final Pattern TIMECODE_LINK_REGEX = Pattern.compile("antennapod://timecode/((\\d+))"); + private static final String TIMECODE_LINK = "<a class=\"timecode\" href=\"antennapod://timecode/%d\">%s</a>"; + private static final Pattern TIMECODE_REGEX = Pattern.compile("\\b(?:(?:(([0-9][0-9])):))?(([0-9][0-9])):(([0-9][0-9]))\\b"); + + /** + * Applies an app-specific CSS stylesheet and adds timecode links (optional). + * <p/> + * This method does NOT change the original shownotes string of the shownotesProvider object and it should + * also not be changed by the caller. + * + * @param addTimecodes True if this method should add timecode links + * @return The processed HTML string. + */ + public String processShownotes(final boolean addTimecodes) { + final Playable playable = (shownotesProvider instanceof Playable) ? (Playable) shownotesProvider : null; + + // load shownotes + + String shownotes; + try { + shownotes = shownotesProvider.loadShownotes().call(); + } catch (Exception e) { + e.printStackTrace(); + return null; + } + if (shownotes == null) { + if (BuildConfig.DEBUG) + Log.d(TAG, "shownotesProvider contained no shownotes. Returning empty string"); + return ""; + } + + Document document = Jsoup.parse(shownotes); + + // apply style + String styleStr = String.format(WEBVIEW_STYLE, colorString, "100%", pageMargin, + pageMargin, pageMargin, pageMargin); + document.head().appendElement("style").attr("type", "text/css").text(styleStr); + + // apply timecode links + if (addTimecodes) { + Elements elementsWithTimeCodes = document.body().getElementsMatchingOwnText(TIMECODE_REGEX); + if (BuildConfig.DEBUG) + Log.d(TAG, "Recognized " + elementsWithTimeCodes.size() + " timecodes"); + for (Element element : elementsWithTimeCodes) { + Matcher matcherLong = TIMECODE_REGEX.matcher(element.text()); + StringBuffer buffer = new StringBuffer(); + while (matcherLong.find()) { + String h = matcherLong.group(1); + String group = matcherLong.group(0); + int time = (h != null) ? Converter.durationStringLongToMs(group) : + Converter.durationStringShortToMs(group); + + String rep; + if (playable == null || playable.getDuration() > time) { + rep = String.format(TIMECODE_LINK, time, group); + } else { + rep = group; + } + matcherLong.appendReplacement(buffer, rep); + } + matcherLong.appendTail(buffer); + + element.html(buffer.toString()); + } + } + + Log.i(TAG, "Out: " + document.toString()); + return document.toString(); + } + + + /** + * Returns true if the given link is a timecode link. + */ + public static boolean isTimecodeLink(String link) { + return link != null && link.matches(TIMECODE_LINK_REGEX.pattern()); + } + + /** + * Returns the time in milliseconds that is attached to this link or -1 + * if the link is no valid timecode link. + */ + public static int getTimecodeLinkTime(String link) { + if (isTimecodeLink(link)) { + Matcher m = TIMECODE_LINK_REGEX.matcher(link); + + try { + if (m.find()) { + return Integer.valueOf(m.group(1)); + } + } catch (NumberFormatException e) { + e.printStackTrace(); + } + } + return -1; + } + + + public void setShownotesProvider(ShownotesProvider shownotesProvider) { + Validate.notNull(shownotesProvider); + this.shownotesProvider = shownotesProvider; + } +} diff --git a/src/de/danoeh/antennapod/util/syndication/FeedDiscoverer.java b/src/de/danoeh/antennapod/util/syndication/FeedDiscoverer.java new file mode 100644 index 000000000..ac38ec876 --- /dev/null +++ b/src/de/danoeh/antennapod/util/syndication/FeedDiscoverer.java @@ -0,0 +1,78 @@ +package de.danoeh.antennapod.util.syndication; + +import android.net.Uri; +import org.apache.commons.lang3.StringUtils; +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; +import org.jsoup.nodes.Element; +import org.jsoup.select.Elements; + +import java.io.File; +import java.io.IOException; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * Finds RSS/Atom URLs in a HTML document using the auto-discovery techniques described here: + * <p/> + * http://www.rssboard.org/rss-autodiscovery + * <p/> + * http://blog.whatwg.org/feed-autodiscovery + */ +public class FeedDiscoverer { + + private static final String MIME_RSS = "application/rss+xml"; + private static final String MIME_ATOM = "application/atom+xml"; + + /** + * Discovers links to RSS and Atom feeds in the given File which must be a HTML document. + * + * @return A map which contains the feed URLs as keys and titles as values (the feed URL is also used as a title if + * a title cannot be found). + */ + public Map<String, String> findLinks(File in, String baseUrl) throws IOException { + return findLinks(Jsoup.parse(in, null), baseUrl); + } + + /** + * Discovers links to RSS and Atom feeds in the given File which must be a HTML document. + * + * @return A map which contains the feed URLs as keys and titles as values (the feed URL is also used as a title if + * a title cannot be found). + */ + public Map<String, String> findLinks(String in, String baseUrl) throws IOException { + return findLinks(Jsoup.parse(in), baseUrl); + } + + private Map<String, String> findLinks(Document document, String baseUrl) { + Map<String, String> res = new LinkedHashMap<String, String>(); + Elements links = document.head().getElementsByTag("link"); + for (Element link : links) { + String rel = link.attr("rel"); + String href = link.attr("href"); + if (!StringUtils.isEmpty(href) && + (rel.equals("alternate") || rel.equals("feed"))) { + String type = link.attr("type"); + if (type.equals(MIME_RSS) || type.equals(MIME_ATOM)) { + String title = link.attr("title"); + String processedUrl = processURL(baseUrl, href); + if (processedUrl != null) { + res.put(processedUrl, + (StringUtils.isEmpty(title)) ? href : title); + } + } + } + } + return res; + } + + private String processURL(String baseUrl, String strUrl) { + Uri uri = Uri.parse(strUrl); + if (uri.isRelative()) { + Uri res = Uri.parse(baseUrl).buildUpon().path(strUrl).build(); + return (res != null) ? res.toString() : null; + } else { + return strUrl; + } + } +} |