diff options
42 files changed, 1068 insertions, 162 deletions
diff --git a/.gitignore b/.gitignore index 153ef778f..482ba1839 100644 --- a/.gitignore +++ b/.gitignore @@ -41,3 +41,4 @@ libs *.DS_Store src/de/danoeh/antennapod/util/flattr/FlattrConfig.java gradle.properties +*.keystore diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/gpodnet/PodcastListAdapter.java b/app/src/main/java/de/danoeh/antennapod/adapter/gpodnet/PodcastListAdapter.java index 58af2c4d5..b85709c5e 100644 --- a/app/src/main/java/de/danoeh/antennapod/adapter/gpodnet/PodcastListAdapter.java +++ b/app/src/main/java/de/danoeh/antennapod/adapter/gpodnet/PodcastListAdapter.java @@ -39,16 +39,15 @@ public class PodcastListAdapter extends ArrayAdapter<GpodnetPodcast> { .getSystemService(Context.LAYOUT_INFLATER_SERVICE); convertView = inflater.inflate(R.layout.gpodnet_podcast_listitem, parent, false); - holder.title = (TextView) convertView.findViewById(R.id.txtvTitle); holder.image = (ImageView) convertView.findViewById(R.id.imgvCover); - + holder.title = (TextView) convertView.findViewById(R.id.txtvTitle); + holder.subscribers = (TextView) convertView.findViewById(R.id.txtvSubscribers); + holder.url = (TextView) convertView.findViewById(R.id.txtvUrl); convertView.setTag(holder); } else { holder = (Holder) convertView.getTag(); } - holder.title.setText(podcast.getTitle()); - if (StringUtils.isNotBlank(podcast.getLogoUrl())) { Picasso.with(convertView.getContext()) .load(podcast.getLogoUrl()) @@ -56,11 +55,17 @@ public class PodcastListAdapter extends ArrayAdapter<GpodnetPodcast> { .into(holder.image); } + holder.title.setText(podcast.getTitle()); + holder.subscribers.setText(String.valueOf(podcast.getSubscribers())); + holder.url.setText(podcast.getUrl()); + return convertView; } static class Holder { - TextView title; ImageView image; + TextView title; + TextView subscribers; + TextView url; } } diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/gpodnet/TagListAdapter.java b/app/src/main/java/de/danoeh/antennapod/adapter/gpodnet/TagListAdapter.java new file mode 100644 index 000000000..b4eadefb5 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/adapter/gpodnet/TagListAdapter.java @@ -0,0 +1,54 @@ +package de.danoeh.antennapod.adapter.gpodnet; + +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.TextView; + +import java.util.List; + +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.core.gpoddernet.model.GpodnetTag; + +/** + * Adapter for displaying a list of GPodnetPodcast-Objects. + */ +public class TagListAdapter extends ArrayAdapter<GpodnetTag> { + + public TagListAdapter(Context context, int resource, List<GpodnetTag> objects) { + super(context, resource, objects); + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + Holder holder; + + GpodnetTag tag = getItem(position); + + // Inflate Layout + if (convertView == null) { + holder = new Holder(); + LayoutInflater inflater = (LayoutInflater) getContext() + .getSystemService(Context.LAYOUT_INFLATER_SERVICE); + + convertView = inflater.inflate(R.layout.gpodnet_tag_listitem, parent, false); + holder.title = (TextView) convertView.findViewById(R.id.txtvTitle); + holder.usage = (TextView) convertView.findViewById(R.id.txtvUsage); + convertView.setTag(holder); + } else { + holder = (Holder) convertView.getTag(); + } + + holder.title.setText(tag.getTitle()); + holder.usage.setText(String.valueOf(tag.getUsage())); + + return convertView; + } + + static class Holder { + TextView title; + TextView usage; + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/itunes/ItunesAdapter.java b/app/src/main/java/de/danoeh/antennapod/adapter/itunes/ItunesAdapter.java new file mode 100644 index 000000000..4fc2838b7 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/adapter/itunes/ItunesAdapter.java @@ -0,0 +1,187 @@ +package de.danoeh.antennapod.adapter.itunes; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.os.AsyncTask; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.ImageView; +import android.widget.TextView; + +import org.apache.http.HttpResponse; +import org.apache.http.client.HttpClient; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.impl.client.DefaultHttpClient; +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.IOException; +import java.util.List; + +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.activity.MainActivity; + +public class ItunesAdapter extends ArrayAdapter<ItunesAdapter.Podcast> { + /** + * Related Context + */ + private final Context context; + + /** + * List holding the podcasts found in the search + */ + private final List<Podcast> data; + + /** + * Constructor. + * + * @param context Related context + * @param objects Search result + */ + public ItunesAdapter(Context context, List<Podcast> objects) { + super(context, 0, objects); + this.data = objects; + this.context = context; + } + + /** + * Updates the given ImageView with the image in the given Podcast's imageUrl + */ + class FetchImageTask extends AsyncTask<Void,Void,Bitmap>{ + /** + * Current podcast + */ + private final Podcast podcast; + + /** + * ImageView to be updated + */ + private final ImageView imageView; + + /** + * Constructor + * + * @param podcast Podcast that has the image + * @param imageView UI image to be updated + */ + FetchImageTask(Podcast podcast, ImageView imageView){ + this.podcast = podcast; + this.imageView = imageView; + } + + //Get the image from the url + @Override + protected Bitmap doInBackground(Void... params) { + HttpClient client = new DefaultHttpClient(); + HttpGet get = new HttpGet(podcast.imageUrl); + try { + HttpResponse response = client.execute(get); + return BitmapFactory.decodeStream(response.getEntity().getContent()); + } catch (IOException e) { + e.printStackTrace(); + } + return null; + } + + //Set the background image for the podcast + @Override + protected void onPostExecute(Bitmap img) { + super.onPostExecute(img); + if(img!=null) { + imageView.setImageBitmap(img); + } + } + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + //Current podcast + Podcast podcast = data.get(position); + + //ViewHolder + PodcastViewHolder viewHolder; + + //Resulting view + View view; + + //Handle view holder stuff + if(convertView == null) { + view = ((MainActivity) context).getLayoutInflater() + .inflate(R.layout.itunes_podcast_listitem, parent, false); + viewHolder = new PodcastViewHolder(view); + view.setTag(viewHolder); + } else { + view = convertView; + viewHolder = (PodcastViewHolder) view.getTag(); + } + + //Set the title + viewHolder.titleView.setText(podcast.title); + + //Update the empty imageView with the image from the feed + new FetchImageTask(podcast,viewHolder.coverView).execute(); + + //Feed the grid view + return view; + } + + /** + * View holder object for the GridView + */ + class PodcastViewHolder { + + /** + * ImageView holding the Podcast image + */ + public final ImageView coverView; + + /** + * TextView holding the Podcast title + */ + public final TextView titleView; + + + /** + * Constructor + * @param view GridView cell + */ + PodcastViewHolder(View view){ + coverView = (ImageView) view.findViewById(R.id.imgvCover); + titleView = (TextView) view.findViewById(R.id.txtvTitle); + } + } + + /** + * Represents an individual podcast on the iTunes Store. + */ + public static class Podcast { //TODO: Move this out eventually. Possibly to core.itunes.model + + /** + * The name of the podcast + */ + public final String title; + + /** + * URL of the podcast image + */ + public final String imageUrl; + /** + * URL of the podcast feed + */ + public final String feedUrl; + + /** + * Constructor. + * + * @param json object holding the podcast information + * @throws JSONException + */ + public Podcast(JSONObject json) throws JSONException { + title = json.getString("collectionName"); + imageUrl = json.getString("artworkUrl100"); + feedUrl = json.getString("feedUrl"); + } + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/AddFeedFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/AddFeedFragment.java index f5ae5a777..e4ae1683b 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/AddFeedFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/AddFeedFragment.java @@ -8,6 +8,7 @@ import android.view.View; import android.view.ViewGroup; import android.widget.Button; import android.widget.EditText; + import de.danoeh.antennapod.R; import de.danoeh.antennapod.activity.DefaultOnlineFeedViewActivity; import de.danoeh.antennapod.activity.MainActivity; @@ -41,10 +42,18 @@ public class AddFeedFragment extends Fragment { Button butBrowserGpoddernet = (Button) root.findViewById(R.id.butBrowseGpoddernet); Button butOpmlImport = (Button) root.findViewById(R.id.butOpmlImport); Button butConfirm = (Button) root.findViewById(R.id.butConfirm); + Button butSearchITunes = (Button) root.findViewById(R.id.butSearchItunes); final MainActivity activity = (MainActivity) getActivity(); activity.getMainActivtyActionBar().setTitle(R.string.add_feed_label); + butSearchITunes.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + activity.loadChildFragment(new ItunesSearchFragment()); + } + }); + butBrowserGpoddernet.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { @@ -53,7 +62,6 @@ public class AddFeedFragment extends Fragment { }); butOpmlImport.setOnClickListener(new View.OnClickListener() { - @Override public void onClick(View v) { startActivity(new Intent(getActivity(), diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/ItunesSearchFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/ItunesSearchFragment.java new file mode 100644 index 000000000..c14b0cc6e --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/fragment/ItunesSearchFragment.java @@ -0,0 +1,193 @@ +package de.danoeh.antennapod.fragment; + +import android.content.Intent; +import android.os.AsyncTask; +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.support.v7.widget.SearchView; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.GridView; + +import org.apache.http.HttpResponse; +import org.apache.http.client.HttpClient; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.impl.client.DefaultHttpClient; +import org.apache.http.util.EntityUtils; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.activity.DefaultOnlineFeedViewActivity; +import de.danoeh.antennapod.activity.OnlineFeedViewActivity; +import de.danoeh.antennapod.adapter.itunes.ItunesAdapter; + +import static de.danoeh.antennapod.adapter.itunes.ItunesAdapter.*; + +//Searches iTunes store for given string and displays results in a list +public class ItunesSearchFragment extends Fragment { + final String TAG = "ItunesSearchFragment"; + /** + * Search input field + */ + private SearchView searchView; + + /** + * Adapter responsible with the search results + */ + private ItunesAdapter adapter; + + /** + * List of podcasts retreived from the search + */ + private List<Podcast> searchResults; + + /** + * Replace adapter data with provided search results from SearchTask. + * @param result List of Podcast objects containing search results + */ + void updateData(List<Podcast> result) { + this.searchResults = result; + adapter.clear(); + + //ArrayAdapter.addAll() requires minsdk > 10 + for(Podcast p: result) { + adapter.add(p); + } + + adapter.notifyDataSetInvalidated(); + } + + /** + * Constructor + */ + public ItunesSearchFragment() { + // Required empty public constructor + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + adapter = new ItunesAdapter(getActivity(), new ArrayList<Podcast>()); + + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + // Inflate the layout for this fragment + View view = inflater.inflate(R.layout.fragment_itunes_search, container, false); + GridView gridView = (GridView) view.findViewById(R.id.gridView); + gridView.setAdapter(adapter); + + //Show information about the podcast when the list item is clicked + gridView.setOnItemClickListener(new AdapterView.OnItemClickListener() { + @Override + public void onItemClick(AdapterView<?> parent, View view, int position, long id) { + Intent intent = new Intent(getActivity(), + DefaultOnlineFeedViewActivity.class); + + //Tell the OnlineFeedViewActivity where to go + String url = searchResults.get(position).feedUrl; + intent.putExtra(OnlineFeedViewActivity.ARG_FEEDURL, url); + + intent.putExtra(DefaultOnlineFeedViewActivity.ARG_TITLE, "iTunes"); + startActivity(intent); + } + }); + + //Configure search input view to be expanded by default with a visible submit button + searchView = (SearchView) view.findViewById(R.id.itunes_search_view); + searchView.setIconifiedByDefault(false); + searchView.setIconified(false); + searchView.setSubmitButtonEnabled(true); + searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() { + @Override + public boolean onQueryTextSubmit(String s) { + //This prevents onQueryTextSubmit() from being called twice when keyboard is used + //to submit the query. + searchView.clearFocus(); + new SearchTask(s).execute(); + return false; + } + + @Override + public boolean onQueryTextChange(String s) { + return false; + } + }); + + return view; + } + + /** + * Search the iTunes store for podcasts using the given query + */ + class SearchTask extends AsyncTask<Void,Void,Void> { + /** + * Incomplete iTunes API search URL + */ + final String apiUrl = "https://itunes.apple.com/search?media=podcast&term=%s"; + + /** + * Search terms + */ + final String query; + + /** + * Search result + */ + final List<Podcast> taskData = new ArrayList<>(); + + /** + * Constructor + * + * @param query Search string + */ + public SearchTask(String query){ + this.query = query; + } + + //Get the podcast data + @Override + protected Void doInBackground(Void... params) { + + //Spaces in the query need to be replaced with '+' character. + String formattedUrl = String.format(apiUrl, query).replace(' ', '+'); + + HttpClient client = new DefaultHttpClient(); + HttpGet get = new HttpGet(formattedUrl); + + try { + HttpResponse response = client.execute(get); + String resultString = EntityUtils.toString(response.getEntity()); + JSONObject result = new JSONObject(resultString); + JSONArray j = result.getJSONArray("results"); + + for (int i = 0; i < j.length(); i++){ + JSONObject podcastJson = j.getJSONObject(i); + Podcast podcast = new Podcast(podcastJson); + taskData.add(podcast); + } + + } catch (IOException | JSONException e) { + e.printStackTrace(); + } + return null; + } + + //Save the data and update the list + @Override + protected void onPostExecute(Void aVoid) { + super.onPostExecute(aVoid); + updateData(taskData); + } + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/QueueFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/QueueFragment.java index 3e60f1af0..da33c6ea3 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/QueueFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/QueueFragment.java @@ -116,13 +116,7 @@ public class QueueFragment extends Fragment { @Override public void onPause() { super.onPause(); - SharedPreferences prefs = getActivity().getSharedPreferences(PREFS, Context.MODE_PRIVATE); - SharedPreferences.Editor editor = prefs.edit(); - View v = listView.getChildAt(0); - int top = (v == null) ? 0 : (v.getTop() - listView.getPaddingTop()); - editor.putInt(PREF_KEY_LIST_SELECTION, listView.getFirstVisiblePosition()); - editor.putInt(PREF_KEY_LIST_TOP, top); - editor.commit(); + saveScrollPosition(); } @Override @@ -138,6 +132,30 @@ public class QueueFragment extends Fragment { this.activity.set((MainActivity) activity); } + private void saveScrollPosition() { + SharedPreferences prefs = getActivity().getSharedPreferences(PREFS, Context.MODE_PRIVATE); + SharedPreferences.Editor editor = prefs.edit(); + View v = listView.getChildAt(0); + int top = (v == null) ? 0 : (v.getTop() - listView.getPaddingTop()); + editor.putInt(PREF_KEY_LIST_SELECTION, listView.getFirstVisiblePosition()); + editor.putInt(PREF_KEY_LIST_TOP, top); + editor.commit(); + } + + private void restoreScrollPosition() { + SharedPreferences prefs = getActivity().getSharedPreferences(PREFS, Context.MODE_PRIVATE); + int listSelection = prefs.getInt(PREF_KEY_LIST_SELECTION, 0); + int top = prefs.getInt(PREF_KEY_LIST_TOP, 0); + if(listSelection > 0 || top > 0) { + listView.setSelectionFromTop(listSelection, top); + // restore once, then forget + SharedPreferences.Editor editor = prefs.edit(); + editor.putInt(PREF_KEY_LIST_SELECTION, 0); + editor.putInt(PREF_KEY_LIST_TOP, 0); + editor.commit(); + } + } + private void resetViewState() { unregisterForContextMenu(listView); listAdapter = null; @@ -374,10 +392,7 @@ public class QueueFragment extends Fragment { } listAdapter.notifyDataSetChanged(); - SharedPreferences prefs = getActivity().getSharedPreferences(PREFS, Context.MODE_PRIVATE); - int listSelection = prefs.getInt(PREF_KEY_LIST_SELECTION, 0); - int top = prefs.getInt(PREF_KEY_LIST_TOP, 0); - listView.setSelectionFromTop(listSelection, top); + restoreScrollPosition(); // we need to refresh the options menu because it sometimes // needs data that may have just been loaded. diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/TagFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/TagFragment.java index c8cdbcfed..e2450f03d 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/TagFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/TagFragment.java @@ -24,11 +24,11 @@ public class TagFragment extends PodcastListFragment { private GpodnetTag tag; - public static TagFragment newInstance(String tagName) { - Validate.notNull(tagName); + public static TagFragment newInstance(GpodnetTag tag) { + Validate.notNull(tag); TagFragment fragment = new TagFragment(); Bundle args = new Bundle(); - args.putString("tag", tagName); + args.putParcelable("tag", tag); fragment.setArguments(args); return fragment; } @@ -38,14 +38,14 @@ public class TagFragment extends PodcastListFragment { super.onCreate(savedInstanceState); Bundle args = getArguments(); - Validate.isTrue(args != null && args.getString("tag") != null, "args invalid"); - tag = new GpodnetTag(args.getString("tag")); + Validate.isTrue(args != null && args.getParcelable("tag") != null, "args invalid"); + tag = args.getParcelable("tag"); } @Override public void onActivityCreated(@Nullable Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); - ((MainActivity) getActivity()).getMainActivtyActionBar().setTitle(tag.getName()); + ((MainActivity) getActivity()).getMainActivtyActionBar().setTitle(tag.getTitle()); } @Override diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/TagListFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/TagListFragment.java index 24e0e4caa..cc87407b4 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/TagListFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/TagListFragment.java @@ -10,14 +10,13 @@ import android.view.Menu; import android.view.MenuInflater; 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.adapter.gpodnet.TagListAdapter; import de.danoeh.antennapod.core.gpoddernet.GpodnetService; import de.danoeh.antennapod.core.gpoddernet.GpodnetServiceException; import de.danoeh.antennapod.core.gpoddernet.model.GpodnetTag; @@ -67,9 +66,9 @@ public class TagListFragment extends ListFragment { getListView().setOnItemClickListener(new AdapterView.OnItemClickListener() { @Override public void onItemClick(AdapterView<?> parent, View view, int position, long id) { - String selectedTag = (String) getListAdapter().getItem(position); + GpodnetTag tag = (GpodnetTag) getListAdapter().getItem(position); MainActivity activity = (MainActivity) getActivity(); - activity.loadChildFragment(TagFragment.newInstance(selectedTag)); + activity.loadChildFragment(TagFragment.newInstance(tag)); } }); @@ -77,6 +76,12 @@ public class TagListFragment extends ListFragment { } @Override + public void onResume() { + super.onResume(); + ((MainActivity) getActivity()).getMainActivtyActionBar().setTitle(R.string.add_feed_label); + } + + @Override public void onDestroyView() { super.onDestroyView(); cancelLoadTask(); @@ -121,11 +126,7 @@ public class TagListFragment extends ListFragment { final Context context = getActivity(); if (context != null) { if (gpodnetTags != null) { - List<String> tagNames = new ArrayList<String>(); - for (GpodnetTag tag : gpodnetTags) { - tagNames.add(tag.getName()); - } - setListAdapter(new ArrayAdapter<String>(context, android.R.layout.simple_list_item_1, tagNames)); + setListAdapter(new TagListAdapter(context, android.R.layout.simple_list_item_1, gpodnetTags)); } else if (exception != null) { TextView txtvError = new TextView(getActivity()); txtvError.setText(exception.getMessage()); diff --git a/app/src/main/java/de/danoeh/antennapod/menuhandler/MenuItemUtils.java b/app/src/main/java/de/danoeh/antennapod/menuhandler/MenuItemUtils.java index 05d6ded4d..fc942ce20 100644 --- a/app/src/main/java/de/danoeh/antennapod/menuhandler/MenuItemUtils.java +++ b/app/src/main/java/de/danoeh/antennapod/menuhandler/MenuItemUtils.java @@ -14,7 +14,7 @@ public class MenuItemUtils extends de.danoeh.antennapod.core.menuhandler.MenuIte public static MenuItem addSearchItem(Menu menu, SearchView searchView) { 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); + MenuItemCompat.setShowAsAction(item, MenuItemCompat.SHOW_AS_ACTION_ALWAYS); MenuItemCompat.setActionView(item, searchView); return item; } diff --git a/app/src/main/java/de/danoeh/antennapod/preferences/CustomEditTextPreference.java b/app/src/main/java/de/danoeh/antennapod/preferences/CustomEditTextPreference.java new file mode 100644 index 000000000..898a56004 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/preferences/CustomEditTextPreference.java @@ -0,0 +1,33 @@ +package de.danoeh.antennapod.preferences; + +import android.app.AlertDialog; +import android.content.Context; +import android.os.Build; +import android.preference.EditTextPreference; +import android.util.AttributeSet; + +import de.danoeh.antennapod.R; + +public class CustomEditTextPreference extends EditTextPreference { + + public CustomEditTextPreference(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + public CustomEditTextPreference(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public CustomEditTextPreference(Context context) { + super(context); + } + + @Override + protected void onPrepareDialogBuilder(AlertDialog.Builder builder) { + if(Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) { + builder.setInverseBackgroundForced(true); + getEditText().setTextColor(getContext().getResources().getColor(R.color.black)); + } + } + +} diff --git a/app/src/main/java/de/danoeh/antennapod/preferences/PreferenceController.java b/app/src/main/java/de/danoeh/antennapod/preferences/PreferenceController.java index 43f942308..227ea8dfb 100644 --- a/app/src/main/java/de/danoeh/antennapod/preferences/PreferenceController.java +++ b/app/src/main/java/de/danoeh/antennapod/preferences/PreferenceController.java @@ -9,10 +9,14 @@ import android.net.wifi.WifiConfiguration; import android.net.wifi.WifiManager; import android.os.Build; import android.preference.CheckBoxPreference; +import android.preference.EditTextPreference; import android.preference.ListPreference; import android.preference.Preference; import android.preference.PreferenceScreen; +import android.text.Editable; +import android.text.TextWatcher; import android.util.Log; +import android.widget.EditText; import android.widget.Toast; import java.io.File; @@ -214,6 +218,52 @@ public class PreferenceController { } } ); + ui.findPreference(UserPreferences.PREF_PARALLEL_DOWNLOADS) + .setOnPreferenceChangeListener( + new Preference.OnPreferenceChangeListener() { + @Override + public boolean onPreferenceChange(Preference preference, Object o) { + if (o instanceof String) { + try { + int value = Integer.valueOf((String) o); + if (1 <= value && value <= 50) { + setParallelDownloadsText(value); + return true; + } + } catch(NumberFormatException e) { + return false; + } + } + return false; + } + } + ); + // validate and set correct value: number of downloads between 1 and 50 (inclusive) + final EditText ev = ((EditTextPreference)ui.findPreference(UserPreferences.PREF_PARALLEL_DOWNLOADS)).getEditText(); + ev.addTextChangedListener(new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) {} + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) {} + + @Override + public void afterTextChanged(Editable s) { + if(s.length() > 0) { + try { + int value = Integer.valueOf(s.toString()); + if (value <= 0) { + ev.setText("1"); + } else if (value > 50) { + ev.setText("50"); + } + } catch(NumberFormatException e) { + ev.setText("6"); + } + ev.setSelection(ev.getText().length()); + } + } + }); ui.findPreference(UserPreferences.PREF_EPISODE_CACHE_SIZE) .setOnPreferenceChangeListener( new Preference.OnPreferenceChangeListener() { @@ -300,6 +350,7 @@ public class PreferenceController { public void onResume() { checkItemVisibility(); + setParallelDownloadsText(UserPreferences.getParallelDownloads()); setEpisodeCacheSizeText(UserPreferences.getEpisodeCacheSize()); setDataFolderText(); updateGpodnetPreferenceScreen(); @@ -379,6 +430,15 @@ public class PreferenceController { .setEnabled(UserPreferences.isEnableAutodownload()); } + + private void setParallelDownloadsText(int downloads) { + final Resources res = ui.getActivity().getResources(); + + String s = Integer.toString(downloads) + + res.getString(R.string.parallel_downloads_suffix); + ui.findPreference(UserPreferences.PREF_PARALLEL_DOWNLOADS).setSummary(s); + } + private void setEpisodeCacheSizeText(int cacheSize) { final Resources res = ui.getActivity().getResources(); diff --git a/app/src/main/res/layout/addfeed.xml b/app/src/main/res/layout/addfeed.xml index 09502eb7b..a740d88cf 100644 --- a/app/src/main/res/layout/addfeed.xml +++ b/app/src/main/res/layout/addfeed.xml @@ -66,12 +66,19 @@ android:layout_margin="8dp" android:text="@string/browse_gpoddernet_label"/> + <Button + android:id="@+id/butSearchItunes" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_below="@id/butBrowseGpoddernet" + android:layout_margin="8dp" + android:text="@string/search_itunes_label"/> <TextView android:id="@+id/txtvOpmlImport" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_below="@id/butBrowseGpoddernet" + android:layout_below="@id/butSearchItunes" android:layout_margin="8dp" style="@style/AntennaPod.TextView.Heading" android:text="@string/opml_import_label"/> diff --git a/app/src/main/res/layout/fragment_itunes_search.xml b/app/src/main/res/layout/fragment_itunes_search.xml new file mode 100644 index 000000000..17ffe349b --- /dev/null +++ b/app/src/main/res/layout/fragment_itunes_search.xml @@ -0,0 +1,26 @@ +<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" +xmlns:tools="http://schemas.android.com/tools" +android:layout_width="match_parent" +android:layout_height="match_parent" +tools:context="de.danoeh.antennapod.activity.ITunesSearchActivity"> +<android.support.v7.widget.SearchView + android:id="@+id/itunes_search_view" + android:layout_height="wrap_content" + android:layout_width="match_parent" + /> +<GridView + android:id="@+id/gridView" + android:layout_below="@id/itunes_search_view" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:clipToPadding="false" + android:columnWidth="200dp" + android:gravity="center" + android:horizontalSpacing="8dp" + android:numColumns="auto_fit" + android:paddingBottom="@dimen/list_vertical_padding" + android:paddingTop="@dimen/list_vertical_padding" + android:stretchMode="columnWidth" + android:verticalSpacing="8dp" + tools:listitem="@layout/gpodnet_podcast_listitem" /> +</RelativeLayout> diff --git a/app/src/main/res/layout/gpodnet_podcast_listitem.xml b/app/src/main/res/layout/gpodnet_podcast_listitem.xml index 2ade8e478..84c6c280e 100644 --- a/app/src/main/res/layout/gpodnet_podcast_listitem.xml +++ b/app/src/main/res/layout/gpodnet_podcast_listitem.xml @@ -23,16 +23,60 @@ tools:src="@drawable/ic_stat_antenna_default" tools:background="@android:color/holo_green_dark" /> + <LinearLayout + android:id="@+id/subscribers_container" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_alignTop="@id/txtvTitle" + android:layout_alignParentRight="true" + android:layout_marginRight="@dimen/listitem_threeline_horizontalpadding" + android:orientation="horizontal"> + + <ImageView + android:id="@+id/imgFeed" + android:layout_width="wrap_content" + android:layout_height="match_parent" + android:layout_marginRight="-4dp" + android:src="?attr/feed" /> + + <TextView + android:id="@+id/txtvSubscribers" + style="@style/AntennaPod.TextView.ListItemSecondaryTitle" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:lines="1" + tools:text="150" + tools:background="@android:color/holo_green_dark" /> + + </LinearLayout> + <TextView android:id="@+id/txtvTitle" style="@style/AntennaPod.TextView.ListItemPrimaryTitle" - android:layout_width="match_parent" + android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_centerVertical="true" - android:layout_marginBottom="@dimen/listitem_threeline_verticalpadding" + android:layout_marginBottom="@dimen/list_vertical_padding" android:layout_marginRight="@dimen/listitem_threeline_horizontalpadding" android:layout_toRightOf="@id/imgvCover" - android:maxLines="1" - tools:text="Podcast title" + android:layout_toLeftOf="@id/subscribers_container" + android:layout_alignTop="@id/imgvCover" + android:lines="1" + tools:text="Title" tools:background="@android:color/holo_green_dark" /> + + <TextView + android:id="@+id/txtvUrl" + style="android:style/TextAppearance.Small" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginRight="@dimen/listitem_threeline_horizontalpadding" + android:layout_toRightOf="@id/imgvCover" + android:layout_below="@id/txtvTitle" + android:textSize="14sp" + android:textColor="?android:attr/textColorSecondary" + android:ellipsize="middle" + android:maxLines="2" + tools:text="http://www.example.com/feed" + tools:background="@android:color/holo_green_dark"/> + </RelativeLayout> diff --git a/app/src/main/res/layout/gpodnet_tag_listitem.xml b/app/src/main/res/layout/gpodnet_tag_listitem.xml new file mode 100644 index 000000000..9e545e59d --- /dev/null +++ b/app/src/main/res/layout/gpodnet_tag_listitem.xml @@ -0,0 +1,34 @@ +<?xml version="1.0" encoding="utf-8"?> + +<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:gravity="center_vertical" + tools:background="@android:color/darker_gray"> + + <TextView + android:id="@+id/txtvTitle" + style="@style/AntennaPod.TextView.ListItemPrimaryTitle" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginLeft="@dimen/listitem_threeline_horizontalpadding" + android:layout_marginTop="@dimen/listitem_threeline_verticalpadding" + android:layout_marginBottom="@dimen/listitem_threeline_verticalpadding" + android:lines="1" + tools:text="Tag Title" + tools:background="@android:color/holo_green_dark" /> + + <TextView + android:id="@+id/txtvUsage" + style="@style/AntennaPod.TextView.ListItemSecondaryTitle" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_alignParentRight="true" + android:layout_marginRight="@dimen/listitem_threeline_horizontalpadding" + android:layout_marginTop="@dimen/listitem_threeline_verticalpadding" + android:layout_marginBottom="@dimen/listitem_threeline_verticalpadding" + tools:text="301" + tools:background="@android:color/holo_green_dark"/> + +</RelativeLayout> diff --git a/app/src/main/res/layout/itunes_podcast_listitem.xml b/app/src/main/res/layout/itunes_podcast_listitem.xml new file mode 100644 index 000000000..41b1f495f --- /dev/null +++ b/app/src/main/res/layout/itunes_podcast_listitem.xml @@ -0,0 +1,38 @@ +<?xml version="1.0" encoding="utf-8"?> + +<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" +xmlns:tools="http://schemas.android.com/tools" +android:layout_width="match_parent" +android:layout_height="@dimen/listitem_threeline_height" +tools:background="@android:color/darker_gray"> + +<ImageView + android:id="@+id/imgvCover" + android:layout_width="@dimen/thumbnail_length_itemlist" + android:layout_height="@dimen/thumbnail_length_itemlist" + android:layout_alignParentLeft="true" + android:layout_centerVertical="true" + android:layout_marginBottom="@dimen/listitem_threeline_verticalpadding" + android:layout_marginLeft="@dimen/listitem_threeline_horizontalpadding" + android:layout_marginRight="8dp" + android:layout_marginTop="@dimen/listitem_threeline_verticalpadding" + android:adjustViewBounds="true" + android:contentDescription="@string/cover_label" + android:cropToPadding="true" + android:scaleType="fitXY" + tools:src="@drawable/ic_stat_antenna_default" + tools:background="@android:color/holo_green_dark" /> + +<TextView + android:id="@+id/txtvTitle" + style="@style/AntennaPod.TextView.ListItemPrimaryTitle" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_centerVertical="true" + android:layout_marginBottom="@dimen/listitem_threeline_verticalpadding" + android:layout_marginRight="@dimen/listitem_threeline_horizontalpadding" + android:layout_toRightOf="@id/imgvCover" + android:maxLines="1" + tools:text="Podcast title" + tools:background="@android:color/holo_green_dark" /> +</RelativeLayout> diff --git a/app/src/main/res/layout/queue_listitem.xml b/app/src/main/res/layout/queue_listitem.xml index 5d41c52cd..d2d51378b 100644 --- a/app/src/main/res/layout/queue_listitem.xml +++ b/app/src/main/res/layout/queue_listitem.xml @@ -9,9 +9,10 @@ <ImageView android:id="@+id/drag_handle" - android:layout_width="24dp" + android:layout_width="104dp" android:layout_height="match_parent" - android:layout_margin="8dp" + android:layout_marginLeft="-32dp" + android:layout_marginRight="-32dp" android:contentDescription="@string/drag_handle_content_description" android:scaleType="center" android:src="?attr/dragview_background" diff --git a/app/src/main/res/menu/feedlist.xml b/app/src/main/res/menu/feedlist.xml index 8d2d9e367..b6512e828 100644 --- a/app/src/main/res/menu/feedlist.xml +++ b/app/src/main/res/menu/feedlist.xml @@ -7,7 +7,7 @@ android:icon="?attr/navigation_refresh" android:menuCategory="container" android:title="@string/refresh_label" - custom:showAsAction="ifRoom"> + custom:showAsAction="always"> </item> <item android:id="@+id/refresh_complete_item" diff --git a/app/src/main/res/menu/new_episodes.xml b/app/src/main/res/menu/new_episodes.xml index d74e70b3b..72661a17e 100644 --- a/app/src/main/res/menu/new_episodes.xml +++ b/app/src/main/res/menu/new_episodes.xml @@ -7,7 +7,7 @@ android:id="@+id/refresh_item" android:title="@string/refresh_label" android:menuCategory="container" - custom:showAsAction="ifRoom" + custom:showAsAction="always" android:icon="?attr/navigation_refresh"/> <item diff --git a/app/src/main/res/menu/queue.xml b/app/src/main/res/menu/queue.xml index 51e47c061..c7dd4d371 100644 --- a/app/src/main/res/menu/queue.xml +++ b/app/src/main/res/menu/queue.xml @@ -7,7 +7,7 @@ android:id="@+id/refresh_item" android:title="@string/refresh_label" android:menuCategory="container" - custom:showAsAction="ifRoom" + custom:showAsAction="always" android:icon="?attr/navigation_refresh"/> <item diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml index cf1be1a74..6d14349d5 100644 --- a/app/src/main/res/xml/preferences.xml +++ b/app/src/main/res/xml/preferences.xml @@ -88,13 +88,17 @@ android:key="prefAutoUpdateIntervall" android:summary="@string/pref_autoUpdateIntervall_sum" android:title="@string/pref_autoUpdateIntervall_title"/> - <CheckBoxPreference android:defaultValue="false" android:enabled="true" android:key="prefMobileUpdate" android:summary="@string/pref_mobileUpdate_sum" android:title="@string/pref_mobileUpdate_title"/> + <de.danoeh.antennapod.preferences.CustomEditTextPreference + android:defaultValue="6" + android:inputType="number" + android:key="prefParallelDownloads" + android:title="@string/pref_parallel_downloads_title"/> <ListPreference android:defaultValue="20" android:entries="@array/episode_cache_size_entries" diff --git a/core/src/main/java/de/danoeh/antennapod/core/feed/FeedItem.java b/core/src/main/java/de/danoeh/antennapod/core/feed/FeedItem.java index c63b61f55..5a4d869e7 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/feed/FeedItem.java +++ b/core/src/main/java/de/danoeh/antennapod/core/feed/FeedItem.java @@ -135,8 +135,8 @@ public class FeedItem extends FeedComponent implements ShownotesProvider, Flattr if (other.media != null) { if (media == null) { setMedia(other.media); - } else if (media.compareWithOther(other)) { - media.updateFromOther(other); + } else if (media.compareWithOther(other.media)) { + media.updateFromOther(other.media); } } if (other.paymentLink != null) { diff --git a/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/GpodnetService.java b/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/GpodnetService.java index 5ee40186f..a353c984a 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/GpodnetService.java +++ b/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/GpodnetService.java @@ -1,5 +1,8 @@ package de.danoeh.antennapod.core.gpoddernet; +import android.os.Build; +import android.util.Log; + import com.squareup.okhttp.Credentials; import com.squareup.okhttp.MediaType; import com.squareup.okhttp.OkHttpClient; @@ -18,16 +21,27 @@ import org.json.JSONObject; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; -import java.net.CookieManager; -import java.net.CookiePolicy; import java.net.MalformedURLException; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; +import java.security.KeyStore; +import java.security.Principal; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.Collection; +import java.util.HashMap; import java.util.LinkedList; import java.util.List; +import java.util.Map; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSocketFactory; +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; +import javax.net.ssl.X509TrustManager; +import javax.security.auth.x500.X500Principal; import de.danoeh.antennapod.core.ClientConfig; import de.danoeh.antennapod.core.gpoddernet.model.GpodnetDevice; @@ -43,6 +57,8 @@ import de.danoeh.antennapod.core.service.download.AntennapodHttpClient; */ public class GpodnetService { + private static final String TAG = "GpodnetService"; + private static final String BASE_SCHEME = "https"; public static final String DEFAULT_BASE_HOST = "gpodder.net"; @@ -56,9 +72,84 @@ public class GpodnetService { public GpodnetService() { httpClient = AntennapodHttpClient.getHttpClient(); + if (Build.VERSION.SDK_INT <= 10) { + Log.d(TAG, "Use custom SSL factory"); + SSLSocketFactory factory = getCustomSslSocketFactory(); + httpClient.setSslSocketFactory(factory); + } BASE_HOST = GpodnetPreferences.getHostname(); } + private synchronized static SSLSocketFactory getCustomSslSocketFactory() { + try { + TrustManagerFactory defaultTrustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + defaultTrustManagerFactory.init((KeyStore) null); // use system keystore + final X509TrustManager defaultTrustManager = (X509TrustManager) defaultTrustManagerFactory.getTrustManagers()[0]; + TrustManager[] customTrustManagers = new TrustManager[]{new X509TrustManager() { + @Override + public java.security.cert.X509Certificate[] getAcceptedIssuers() { + return null; + } + @Override + public void checkClientTrusted(java.security.cert.X509Certificate[] chain, String authType) throws CertificateException { + } + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType) + throws CertificateException { + // chain may out of order - construct data structures to walk from server certificate to root certificate + Map<Principal, X509Certificate> certificates = new HashMap<Principal, X509Certificate>(chain.length - 1); + X509Certificate subject = null; + for (X509Certificate cert : chain) { + cert.checkValidity(); + if (cert.getSubjectDN().toString().startsWith("CN=" + DEFAULT_BASE_HOST)) { + subject = cert; + } else { + certificates.put(cert.getSubjectDN(), cert); + } + } + if (subject == null) { + throw new CertificateException("Chain does not contain a certificate for " + DEFAULT_BASE_HOST); + } + // follow chain to root CA + while (certificates.get(subject.getIssuerDN()) != null) { + subject.checkValidity(); + X509Certificate issuer = certificates.get(subject.getIssuerDN()); + try { + subject.verify(issuer.getPublicKey()); + } catch (Exception e) { + Log.d(TAG, "failed: " + issuer.getSubjectDN() + " -> " + subject.getSubjectDN()); + throw new CertificateException("Could not verify certificate"); + } + subject = issuer; + } + X500Principal rootAuthority = subject.getIssuerX500Principal(); + boolean accepted = false; + for (X509Certificate cert : + defaultTrustManager.getAcceptedIssuers()) { + if (cert.getSubjectX500Principal().equals(rootAuthority)) { + try { + subject.verify(cert.getPublicKey()); + accepted = true; + } catch (Exception e) { + Log.d(TAG, "failed: " + cert.getSubjectDN() + " -> " + subject.getSubjectDN()); + throw new CertificateException("Could not verify root certificate"); + } + } + } + if (accepted == false) { + throw new CertificateException("Could not verify root certificate"); + } + } + }}; + SSLContext sslContext = SSLContext.getInstance("TLS"); + sslContext.init(null, customTrustManagers, null); + return sslContext.getSocketFactory(); + } catch (Exception e) { + e.printStackTrace(); + } + return null; + } + /** * Returns the [count] most used tags. */ @@ -81,9 +172,10 @@ public class GpodnetService { jsonTagList.length()); for (int i = 0; i < jsonTagList.length(); i++) { JSONObject jObj = jsonTagList.getJSONObject(i); - String name = jObj.getString("tag"); + String title = jObj.getString("title"); + String tag = jObj.getString("tag"); int usage = jObj.getInt("usage"); - tagList.add(new GpodnetTag(name, usage)); + tagList.add(new GpodnetTag(title, tag, usage)); } return tagList; } catch (JSONException e) { @@ -103,7 +195,7 @@ public class GpodnetService { try { URL url = new URI(BASE_SCHEME, BASE_HOST, String.format( - "/api/2/tag/%s/%d.json", tag.getName(), count), null).toURL(); + "/api/2/tag/%s/%d.json", tag.getTag(), count), null).toURL(); Request.Builder request = new Request.Builder().url(url); String response = executeRequest(request); diff --git a/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetTag.java b/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetTag.java index 7178f4be5..cd865731b 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetTag.java +++ b/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetTag.java @@ -1,46 +1,60 @@ package de.danoeh.antennapod.core.gpoddernet.model; -import org.apache.commons.lang3.Validate; +import android.os.Parcel; +import android.os.Parcelable; -import java.util.Comparator; +import org.apache.commons.lang3.Validate; -public class GpodnetTag { +public class GpodnetTag implements Parcelable { - private String name; - private int usage; + private final String title; + private final String tag; + private final int usage; - public GpodnetTag(String name, int usage) { - Validate.notNull(name); + public GpodnetTag(String title, String tag, int usage) { + Validate.notNull(title); + Validate.notNull(tag); - this.name = name; + this.title = title; + this.tag = tag; this.usage = usage; } - public GpodnetTag(String name) { - super(); - this.name = name; + public static GpodnetTag createFromParcel(Parcel in) { + final String title = in.readString(); + final String tag = in.readString(); + final int usage = in.readInt(); + return new GpodnetTag(title, tag, usage); } @Override public String toString() { - return "GpodnetTag [name=" + name + ", usage=" + usage + "]"; + return "GpodnetTag [title="+title+", tag=" + tag + ", usage=" + usage + "]"; } - public String getName() { - return name; + public String getTitle() { + return title; + } + + public String getTag() { + return tag; } public int getUsage() { return usage; } - public static class UsageComparator implements Comparator<GpodnetTag> { - - @Override - public int compare(GpodnetTag o1, GpodnetTag o2) { - return o1.usage - o2.usage; - } + @Override + public int describeContents() { + return 0; + } + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(title); + dest.writeString(tag); + dest.writeInt(usage); } + } diff --git a/core/src/main/java/de/danoeh/antennapod/core/preferences/UserPreferences.java b/core/src/main/java/de/danoeh/antennapod/core/preferences/UserPreferences.java index 7cbb69a7f..6cb2faba5 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/preferences/UserPreferences.java +++ b/core/src/main/java/de/danoeh/antennapod/core/preferences/UserPreferences.java @@ -20,7 +20,6 @@ import java.util.LinkedList; import java.util.List; import java.util.concurrent.TimeUnit; -import de.danoeh.antennapod.core.ApplicationCallbacks; import de.danoeh.antennapod.core.BuildConfig; import de.danoeh.antennapod.core.ClientConfig; import de.danoeh.antennapod.core.R; @@ -42,6 +41,7 @@ public class UserPreferences implements 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_PARALLEL_DOWNLOADS = "prefParallelDownloads"; 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"; @@ -86,6 +86,7 @@ public class UserPreferences implements private boolean enableAutodownloadWifiFilter; private boolean enableAutodownloadOnBattery; private String[] autodownloadSelectedNetworks; + private int parallelDownloads; private int episodeCacheSize; private String playbackSpeed; private String[] playbackSpeedArray; @@ -144,6 +145,7 @@ public class UserPreferences implements PREF_ENABLE_AUTODL_WIFI_FILTER, false); autodownloadSelectedNetworks = StringUtils.split( sp.getString(PREF_AUTODL_SELECTED_NETWORKS, ""), ','); + parallelDownloads = Integer.valueOf(sp.getString(PREF_PARALLEL_DOWNLOADS, "6")); episodeCacheSize = readEpisodeCacheSizeInternal(sp.getString( PREF_EPISODE_CACHE_SIZE, "20")); enableAutodownload = sp.getBoolean(PREF_ENABLE_AUTODL, false); @@ -314,6 +316,11 @@ public class UserPreferences implements return instance.autodownloadSelectedNetworks; } + public static int getParallelDownloads() { + instanceAvailable(); + return instance.parallelDownloads; + } + public static int getEpisodeCacheSizeUnlimited() { return EPISODE_CACHE_SIZE_UNLIMITED; } @@ -399,6 +406,8 @@ public class UserPreferences implements } else if (key.equals(PREF_AUTODL_SELECTED_NETWORKS)) { autodownloadSelectedNetworks = StringUtils.split( sp.getString(PREF_AUTODL_SELECTED_NETWORKS, ""), ','); + } else if(key.equals(PREF_PARALLEL_DOWNLOADS)) { + parallelDownloads = Integer.valueOf(sp.getString(PREF_PARALLEL_DOWNLOADS, "6")); } else if (key.equals(PREF_EPISODE_CACHE_SIZE)) { episodeCacheSize = readEpisodeCacheSizeInternal(sp.getString( PREF_EPISODE_CACHE_SIZE, "20")); diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadService.java b/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadService.java index 02a6aecbd..60d463178 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadService.java +++ b/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadService.java @@ -52,7 +52,6 @@ import java.util.concurrent.atomic.AtomicInteger; import javax.xml.parsers.ParserConfigurationException; -import de.danoeh.antennapod.core.BuildConfig; import de.danoeh.antennapod.core.ClientConfig; import de.danoeh.antennapod.core.R; import de.danoeh.antennapod.core.feed.EventDistributor; @@ -61,6 +60,7 @@ import de.danoeh.antennapod.core.feed.FeedImage; import de.danoeh.antennapod.core.feed.FeedItem; import de.danoeh.antennapod.core.feed.FeedMedia; import de.danoeh.antennapod.core.feed.FeedPreferences; +import de.danoeh.antennapod.core.preferences.UserPreferences; import de.danoeh.antennapod.core.storage.DBReader; import de.danoeh.antennapod.core.storage.DBTasks; import de.danoeh.antennapod.core.storage.DBWriter; @@ -172,12 +172,11 @@ public class DownloadService extends Service { @Override public void run() { - if (BuildConfig.DEBUG) Log.d(TAG, "downloadCompletionThread was started"); + Log.d(TAG, "downloadCompletionThread was started"); while (!isInterrupted()) { try { Downloader downloader = downloadExecutor.take().get(); - if (BuildConfig.DEBUG) - Log.d(TAG, "Received 'Download Complete' - message."); + Log.d(TAG, "Received 'Download Complete' - message."); removeDownload(downloader); DownloadStatus status = downloader.getResult(); boolean successful = status.isSuccessful(); @@ -213,13 +212,13 @@ public class DownloadService extends Service { queryDownloadsAsync(); } } catch (InterruptedException e) { - if (BuildConfig.DEBUG) Log.d(TAG, "DownloadCompletionThread was interrupted"); + Log.d(TAG, "DownloadCompletionThread was interrupted"); } catch (ExecutionException e) { e.printStackTrace(); numberOfDownloads.decrementAndGet(); } } - if (BuildConfig.DEBUG) Log.d(TAG, "End of downloadCompletionThread"); + Log.d(TAG, "End of downloadCompletionThread"); } }; @@ -236,8 +235,7 @@ public class DownloadService extends Service { @SuppressLint("NewApi") @Override public void onCreate() { - if (BuildConfig.DEBUG) - Log.d(TAG, "Service started"); + Log.d(TAG, "Service started"); isRunning = true; handler = new Handler(); newMediaFiles = Collections.synchronizedList(new ArrayList<Long>()); @@ -258,8 +256,9 @@ public class DownloadService extends Service { return t; } }); + Log.d(TAG, "parallel downloads: " + UserPreferences.getParallelDownloads()); downloadExecutor = new ExecutorCompletionService<Downloader>( - Executors.newFixedThreadPool(NUM_PARALLEL_DOWNLOADS, + Executors.newFixedThreadPool(UserPreferences.getParallelDownloads(), new ThreadFactory() { @Override @@ -304,8 +303,7 @@ public class DownloadService extends Service { @Override public void onDestroy() { - if (BuildConfig.DEBUG) - Log.d(TAG, "Service shutting down"); + Log.d(TAG, "Service shutting down"); isRunning = false; if (ClientConfig.downloadServiceCallbacks.shouldCreateReport()) { @@ -346,8 +344,7 @@ public class DownloadService extends Service { .setLargeIcon(icon) .setSmallIcon(R.drawable.stat_notify_sync); } - if (BuildConfig.DEBUG) - Log.d(TAG, "Notification set up"); + Log.d(TAG, "Notification set up"); } /** @@ -427,8 +424,7 @@ public class DownloadService extends Service { String url = intent.getStringExtra(EXTRA_DOWNLOAD_URL); Validate.notNull(url, "ACTION_CANCEL_DOWNLOAD intent needs download url extra"); - if (BuildConfig.DEBUG) - Log.d(TAG, "Cancelling download with url " + url); + Log.d(TAG, "Cancelling download with url " + url); Downloader d = getDownloader(url); if (d != null) { d.cancel(); @@ -439,8 +435,7 @@ public class DownloadService extends Service { } else if (StringUtils.equals(intent.getAction(), ACTION_CANCEL_ALL_DOWNLOADS)) { for (Downloader d : downloads) { d.cancel(); - if (BuildConfig.DEBUG) - Log.d(TAG, "Cancelled all downloads"); + Log.d(TAG, "Cancelled all downloads"); } sendBroadcast(new Intent(ACTION_DOWNLOADS_CONTENT_CHANGED)); @@ -451,8 +446,7 @@ public class DownloadService extends Service { }; private void onDownloadQueued(Intent intent) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Received enqueue request"); + Log.d(TAG, "Received enqueue request"); DownloadRequest request = intent.getParcelableExtra(EXTRA_REQUEST); if (request == null) { throw new IllegalArgumentException( @@ -462,7 +456,12 @@ public class DownloadService extends Service { Downloader downloader = getDownloader(request); if (downloader != null) { numberOfDownloads.incrementAndGet(); - downloads.add(downloader); + // smaller rss feeds before bigger media files + if(request.getFeedfileId() == Feed.FEEDFILETYPE_FEED) { + downloads.add(0, downloader); + } else { + downloads.add(downloader); + } downloadExecutor.submit(downloader); sendBroadcast(new Intent(ACTION_DOWNLOADS_CONTENT_CHANGED)); } @@ -490,12 +489,10 @@ public class DownloadService extends Service { handler.post(new Runnable() { @Override public void run() { - if (BuildConfig.DEBUG) - Log.d(TAG, "Removing downloader: " - + d.getDownloadRequest().getSource()); + Log.d(TAG, "Removing downloader: " + + d.getDownloadRequest().getSource()); boolean rc = downloads.remove(d); - if (BuildConfig.DEBUG) - Log.d(TAG, "Result of downloads.remove: " + rc); + Log.d(TAG, "Result of downloads.remove: " + rc); DownloadRequester.getInstance().removeDownload(d.getDownloadRequest()); sendBroadcast(new Intent(ACTION_DOWNLOADS_CONTENT_CHANGED)); } @@ -544,8 +541,7 @@ public class DownloadService extends Service { } if (createReport) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Creating report"); + Log.d(TAG, "Creating report"); // create notification object Notification notification = new NotificationCompat.Builder(this) .setTicker( @@ -569,8 +565,7 @@ public class DownloadService extends Service { NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); nm.notify(REPORT_ID, notification); } else { - if (BuildConfig.DEBUG) - Log.d(TAG, "No report is created"); + Log.d(TAG, "No report is created"); } reportQueue.clear(); } @@ -592,13 +587,10 @@ public class DownloadService extends Service { * Check if there's something else to download, otherwise stop */ void queryDownloads() { - if (BuildConfig.DEBUG) { - Log.d(TAG, numberOfDownloads.get() + " downloads left"); - } + Log.d(TAG, numberOfDownloads.get() + " downloads left"); if (numberOfDownloads.get() <= 0 && DownloadRequester.getInstance().hasNoDownloads()) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Number of downloads is " + numberOfDownloads.get() + ", attempting shutdown"); + Log.d(TAG, "Number of downloads is " + numberOfDownloads.get() + ", attempting shutdown"); stopSelf(); } else { setupNotificationUpdater(); @@ -634,8 +626,7 @@ public class DownloadService extends Service { * Is called whenever a Feed is downloaded */ private void handleCompletedFeedDownload(DownloadRequest request) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Handling completed Feed Download"); + Log.d(TAG, "Handling completed Feed Download"); feedSyncThread.submitCompletedDownload(request); } @@ -644,8 +635,7 @@ public class DownloadService extends Service { * Is called whenever a Feed-Image is downloaded */ private void handleCompletedImageDownload(DownloadStatus status, DownloadRequest request) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Handling completed Image Download"); + Log.d(TAG, "Handling completed Image Download"); syncExecutor.execute(new ImageHandlerThread(status, request)); } @@ -653,13 +643,12 @@ public class DownloadService extends Service { * Is called whenever a FeedMedia is downloaded. */ private void handleCompletedFeedMediaDownload(DownloadStatus status, DownloadRequest request) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Handling completed FeedMedia Download"); + Log.d(TAG, "Handling completed FeedMedia Download"); syncExecutor.execute(new MediaHandlerThread(status, request)); } private void handleFailedDownload(DownloadStatus status, DownloadRequest request) { - if (BuildConfig.DEBUG) Log.d(TAG, "Handling failed download"); + Log.d(TAG, "Handling failed download"); syncExecutor.execute(new FailedDownloadHandler(status, request)); } @@ -709,12 +698,10 @@ public class DownloadService extends Service { long currentTime = startTime; while (requester.isDownloadingFeeds() && (currentTime - startTime) < WAIT_TIMEOUT) { try { - if (BuildConfig.DEBUG) - Log.d(TAG, "Waiting for " + (startTime + WAIT_TIMEOUT - currentTime) + " ms"); + 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"); + Log.d(TAG, "interrupted while waiting for more downloads"); tasks += pollCompletedDownloads(); } finally { currentTime = System.currentTimeMillis(); @@ -762,7 +749,7 @@ public class DownloadService extends Service { continue; } - if (BuildConfig.DEBUG) Log.d(TAG, "Bundling " + results.size() + " feeds"); + Log.d(TAG, "Bundling " + results.size() + " feeds"); for (Pair<DownloadRequest, FeedHandlerResult> result : results) { removeDuplicateImages(result.second.feed); // duplicate images have to removed because the DownloadRequester does not accept two downloads with the same download URL yet. @@ -789,8 +776,7 @@ public class DownloadService extends Service { // Download Feed Image if provided and not downloaded if (savedFeed.getImage() != null && savedFeed.getImage().isDownloaded() == false) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Feed has image; Downloading...."); + Log.d(TAG, "Feed has image; Downloading...."); savedFeed.getImage().setOwner(savedFeed); final Feed savedFeedRef = savedFeed; try { @@ -856,7 +842,7 @@ public class DownloadService extends Service { } - if (BuildConfig.DEBUG) Log.d(TAG, "Shutting down"); + Log.d(TAG, "Shutting down"); } @@ -902,8 +888,7 @@ public class DownloadService extends Service { FeedHandlerResult result = null; try { result = feedHandler.parseFeed(feed); - if (BuildConfig.DEBUG) - Log.d(TAG, feed.getTitle() + " parsed"); + Log.d(TAG, feed.getTitle() + " parsed"); if (checkFeedData(feed) == false) { throw new InvalidFeedException(); } @@ -1008,13 +993,13 @@ public class DownloadService extends Service { */ private void cleanup(Feed feed) { if (feed.getFile_url() != null) { - if (new File(feed.getFile_url()).delete()) - if (BuildConfig.DEBUG) - Log.d(TAG, "Successfully deleted cache file."); - else - Log.e(TAG, "Failed to delete cache file."); + if (new File(feed.getFile_url()).delete()) { + Log.d(TAG, "Successfully deleted cache file."); + } else { + Log.e(TAG, "Failed to delete cache file."); + } feed.setFile_url(null); - } else if (BuildConfig.DEBUG) { + } else { Log.d(TAG, "Didn't delete cache file: File url is not set."); } } @@ -1056,7 +1041,7 @@ public class DownloadService extends Service { @Override public void run() { if (request.isDeleteOnFailure()) { - if (BuildConfig.DEBUG) Log.d(TAG, "Ignoring failed download, deleteOnFailure=true"); + Log.d(TAG, "Ignoring failed download, deleteOnFailure=true"); } else { File dest = new File(request.getDestination()); if (dest.exists() && request.getFeedfileType() == FeedMedia.FEEDFILETYPE_FEEDMEDIA) { @@ -1144,8 +1129,7 @@ public class DownloadService extends Service { mmr.setDataSource(media.getFile_url()); String durationStr = mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION); media.setDuration(Integer.parseInt(durationStr)); - if (BuildConfig.DEBUG) - Log.d(TAG, "Duration of file is " + media.getDuration()); + Log.d(TAG, "Duration of file is " + media.getDuration()); } catch (NumberFormatException e) { e.printStackTrace(); } catch (RuntimeException e) { @@ -1191,8 +1175,7 @@ public class DownloadService extends Service { * Schedules the notification updater task if it hasn't been scheduled yet. */ private void setupNotificationUpdater() { - if (BuildConfig.DEBUG) - Log.d(TAG, "Setting up notification updater"); + Log.d(TAG, "Setting up notification updater"); if (notificationUpdater == null) { notificationUpdater = new NotificationUpdater(); notificationUpdaterFuture = schedExecutor.scheduleAtFixedRate( diff --git a/core/src/main/java/de/danoeh/antennapod/core/syndication/handler/SyndHandler.java b/core/src/main/java/de/danoeh/antennapod/core/syndication/handler/SyndHandler.java index 1dda24944..47503dee4 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/syndication/handler/SyndHandler.java +++ b/core/src/main/java/de/danoeh/antennapod/core/syndication/handler/SyndHandler.java @@ -1,14 +1,23 @@ package de.danoeh.antennapod.core.syndication.handler; import android.util.Log; -import de.danoeh.antennapod.core.BuildConfig; -import de.danoeh.antennapod.core.feed.Feed; -import de.danoeh.antennapod.core.syndication.namespace.*; -import de.danoeh.antennapod.core.syndication.namespace.atom.NSAtom; + import org.xml.sax.Attributes; import org.xml.sax.SAXException; import org.xml.sax.helpers.DefaultHandler; +import de.danoeh.antennapod.core.BuildConfig; +import de.danoeh.antennapod.core.feed.Feed; +import de.danoeh.antennapod.core.syndication.namespace.NSContent; +import de.danoeh.antennapod.core.syndication.namespace.NSDublinCore; +import de.danoeh.antennapod.core.syndication.namespace.NSITunes; +import de.danoeh.antennapod.core.syndication.namespace.NSMedia; +import de.danoeh.antennapod.core.syndication.namespace.NSRSS20; +import de.danoeh.antennapod.core.syndication.namespace.NSSimpleChapters; +import de.danoeh.antennapod.core.syndication.namespace.Namespace; +import de.danoeh.antennapod.core.syndication.namespace.SyndElement; +import de.danoeh.antennapod.core.syndication.namespace.atom.NSAtom; + /** Superclass for all SAX Handlers which process Syndication formats */ public class SyndHandler extends DefaultHandler { private static final String TAG = "SyndHandler"; @@ -100,7 +109,12 @@ public class SyndHandler extends DefaultHandler { state.namespaces.put(uri, new NSMedia()); if (BuildConfig.DEBUG) Log.d(TAG, "Recognized media namespace"); - } + } else if (uri.equals(NSDublinCore.NSURI) + && prefix.equals(NSDublinCore.NSTAG)) { + state.namespaces.put(uri, new NSDublinCore()); + if (BuildConfig.DEBUG) + Log.d(TAG, "Recognized DublinCore namespace"); + } } } diff --git a/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSDublinCore.java b/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSDublinCore.java new file mode 100644 index 000000000..099593eed --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSDublinCore.java @@ -0,0 +1,37 @@ +package de.danoeh.antennapod.core.syndication.namespace; + +import org.xml.sax.Attributes; + +import de.danoeh.antennapod.core.syndication.handler.HandlerState; +import de.danoeh.antennapod.core.syndication.util.SyndDateUtils; + +public class NSDublinCore extends Namespace { + private static final String TAG = "NSDublinCore"; + public static final String NSTAG = "dc"; + public static final String NSURI = "http://purl.org/dc/elements/1.1/"; + + private static final String ITEM = "item"; + private static final String DATE = "date"; + + @Override + public SyndElement handleElementStart(String localName, HandlerState state, + Attributes attributes) { + return new SyndElement(localName, this); + } + + @Override + public void handleElementEnd(String localName, HandlerState state) { + if(state.getTagstack().size() >= 2 + && state.getContentBuf() != null) { + String content = state.getContentBuf().toString(); + SyndElement topElement = state.getTagstack().peek(); + String top = topElement.getName(); + SyndElement secondElement = state.getSecondTag(); + String second = secondElement.getName(); + if (top.equals(DATE) && second.equals(ITEM)) { + state.getCurrentItem().setPubDate( + SyndDateUtils.parseISO8601Date(content)); + } + } + } +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/syndication/util/SyndDateUtils.java b/core/src/main/java/de/danoeh/antennapod/core/syndication/util/SyndDateUtils.java index 1ac389f08..a9929d7b1 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/syndication/util/SyndDateUtils.java +++ b/core/src/main/java/de/danoeh/antennapod/core/syndication/util/SyndDateUtils.java @@ -28,6 +28,8 @@ public class SyndDateUtils { */ public static final String RFC3339LOCAL = "yyyy-MM-dd'T'HH:mm:ssZ"; + public static final String ISO8601_SHORT = "yyyy-MM-dd"; + private static ThreadLocal<SimpleDateFormat> RFC822Formatter = new ThreadLocal<SimpleDateFormat>() { @Override protected SimpleDateFormat initialValue() { @@ -44,6 +46,14 @@ public class SyndDateUtils { }; + private static ThreadLocal<SimpleDateFormat> ISO8601ShortFormatter = new ThreadLocal<SimpleDateFormat>() { + @Override + protected SimpleDateFormat initialValue() { + return new SimpleDateFormat(ISO8601_SHORT, Locale.US); + } + + }; + public static Date parseRFC822Date(String date) { Date result = null; if (date.contains("PDT")) { @@ -123,6 +133,23 @@ public class SyndDateUtils { } + public static Date parseISO8601Date(String date) { + if(date.length() > ISO8601_SHORT.length()) { + return parseRFC3339Date(date); + } + Date result = null; + if(date.length() == "YYYYMMDD".length()) { + date = date.substring(0, 4) + "-" + date.substring(4, 6) + "-" + date.substring(6,8); + } + SimpleDateFormat format = ISO8601ShortFormatter.get(); + try { + result = format.parse(date); + } catch (ParseException e) { + e.printStackTrace(); + } + return result; + } + /** * Takes a string of the form [HH:]MM:SS[.mmm] and converts it to * milliseconds. diff --git a/core/src/main/res/drawable-hdpi/ic_feed_grey600_24dp.png b/core/src/main/res/drawable-hdpi/ic_feed_grey600_24dp.png Binary files differnew file mode 100755 index 000000000..46be3e14e --- /dev/null +++ b/core/src/main/res/drawable-hdpi/ic_feed_grey600_24dp.png diff --git a/core/src/main/res/drawable-hdpi/ic_feed_white_24dp.png b/core/src/main/res/drawable-hdpi/ic_feed_white_24dp.png Binary files differnew file mode 100755 index 000000000..3d57127f5 --- /dev/null +++ b/core/src/main/res/drawable-hdpi/ic_feed_white_24dp.png diff --git a/core/src/main/res/drawable-mdpi/ic_feed_grey600_24dp.png b/core/src/main/res/drawable-mdpi/ic_feed_grey600_24dp.png Binary files differnew file mode 100755 index 000000000..79f082610 --- /dev/null +++ b/core/src/main/res/drawable-mdpi/ic_feed_grey600_24dp.png diff --git a/core/src/main/res/drawable-mdpi/ic_feed_white_24dp.png b/core/src/main/res/drawable-mdpi/ic_feed_white_24dp.png Binary files differnew file mode 100755 index 000000000..15a4b16bf --- /dev/null +++ b/core/src/main/res/drawable-mdpi/ic_feed_white_24dp.png diff --git a/core/src/main/res/drawable-xhdpi/ic_feed_grey600_24dp.png b/core/src/main/res/drawable-xhdpi/ic_feed_grey600_24dp.png Binary files differnew file mode 100755 index 000000000..5cb0262ee --- /dev/null +++ b/core/src/main/res/drawable-xhdpi/ic_feed_grey600_24dp.png diff --git a/core/src/main/res/drawable-xhdpi/ic_feed_white_24dp.png b/core/src/main/res/drawable-xhdpi/ic_feed_white_24dp.png Binary files differnew file mode 100755 index 000000000..5f34b0492 --- /dev/null +++ b/core/src/main/res/drawable-xhdpi/ic_feed_white_24dp.png diff --git a/core/src/main/res/drawable-xxhdpi/ic_feed_grey600_24dp.png b/core/src/main/res/drawable-xxhdpi/ic_feed_grey600_24dp.png Binary files differnew file mode 100755 index 000000000..01ef2ee4d --- /dev/null +++ b/core/src/main/res/drawable-xxhdpi/ic_feed_grey600_24dp.png diff --git a/core/src/main/res/drawable-xxhdpi/ic_feed_white_24dp.png b/core/src/main/res/drawable-xxhdpi/ic_feed_white_24dp.png Binary files differnew file mode 100755 index 000000000..6dd465852 --- /dev/null +++ b/core/src/main/res/drawable-xxhdpi/ic_feed_white_24dp.png diff --git a/core/src/main/res/values-zh-rCN/strings.xml b/core/src/main/res/values-zh-rCN/strings.xml index 59958ec35..5d956bef4 100644 --- a/core/src/main/res/values-zh-rCN/strings.xml +++ b/core/src/main/res/values-zh-rCN/strings.xml @@ -3,11 +3,11 @@ <!--Activitiy and fragment titles--> <string name="app_name">AntennaPod</string> <string name="feeds_label">订阅</string> - <string name="add_feed_label">添加博客</string> + <string name="add_feed_label">添加播客</string> <string name="podcasts_label">播客</string> - <string name="episodes_label">曲目</string> - <string name="new_episodes_label">新曲目</string> - <string name="all_episodes_label">所有曲目</string> + <string name="episodes_label">单集</string> + <string name="new_episodes_label">新单集</string> + <string name="all_episodes_label">所有单集</string> <string name="new_label">最新</string> <string name="waiting_list_label">等待列表</string> <string name="settings_label">设置</string> @@ -22,7 +22,7 @@ <string name="gpodnet_auth_label">gpodder.net 登录</string> <!--New episodes fragment--> <string name="recently_published_episodes_label">最近发布</string> - <string name="episode_filter_label">仅显示新曲目</string> + <string name="episode_filter_label">仅显示新单集</string> <!--Main activity--> <string name="drawer_open">打开菜单</string> <string name="drawer_close">关闭菜单</string> @@ -47,7 +47,7 @@ <string name="chapters_label">章节</string> <string name="shownotes_label">笔记</string> <string name="description_label">描述</string> - <string name="most_recent_prefix">最近曲目:\u0020</string> + <string name="most_recent_prefix">最近单集:\u0020</string> <string name="episodes_suffix">\u0020 曲</string> <string name="length_prefix">长度:\u0020</string> <string name="size_prefix">大小:\u0020</string> @@ -64,12 +64,12 @@ <string name="browse_gpoddernet_label">浏览 gpodder.net</string> <!--Actions on feeds--> <string name="mark_all_read_label">全部标识已读</string> - <string name="mark_all_read_msg">将所有曲目标记为已读</string> + <string name="mark_all_read_msg">将所有单集标记为已读</string> <string name="show_info_label">查看信息</string> <string name="remove_feed_label">删除播客</string> <string name="share_link_label">分享网站链接</string> <string name="share_source_label">分享订阅链接</string> - <string name="feed_delete_confirmation_msg">确认要删除这些订阅吗? 该订阅所有已经下载的曲目将一并删除. </string> + <string name="feed_delete_confirmation_msg">确认要删除这些订阅吗? 该订阅所有已经下载的单集将一并删除. </string> <string name="feed_remover_msg">删除订阅</string> <!--actions on feeditems--> <string name="download_label">下载</string> @@ -77,7 +77,7 @@ <string name="pause_label">暂停</string> <string name="stream_label">流媒体</string> <string name="remove_label">删除</string> - <string name="remove_episode_lable">移除曲目</string> + <string name="remove_episode_lable">移除单集</string> <string name="mark_read_label">标记已读</string> <string name="mark_unread_label">标记未读</string> <string name="add_to_queue_label">添加到播放列表</string> @@ -86,7 +86,7 @@ <string name="support_label">Flattr 他</string> <string name="enqueue_all_new">全部添加到播放列表</string> <string name="download_all">全部下载</string> - <string name="skip_episode_label">跳过曲目</string> + <string name="skip_episode_label">跳过单集</string> <!--Download messages and labels--> <string name="download_successful">成功</string> <string name="download_failed">失败</string> @@ -105,7 +105,7 @@ <string name="cancel_all_downloads_label">取消所有下载</string> <string name="download_cancelled_msg">已取消下载</string> <string name="download_report_title">下载完成</string> - <string name="download_error_malformed_url">畸形 URL</string> + <string name="download_error_malformed_url">错误的 URL</string> <string name="download_error_io_error">IO 错误</string> <string name="download_error_request_error">请求出错</string> <string name="download_error_db_access">数据库访问错误</string> @@ -190,10 +190,10 @@ <string name="pref_set_theme_title">主题选择</string> <string name="pref_set_theme_sum">改变 AntennaPod 外观</string> <string name="pref_automatic_download_title">自动下载</string> - <string name="pref_automatic_download_sum">配置自动下载的曲目</string> + <string name="pref_automatic_download_sum">配置自动下载的单集</string> <string name="pref_autodl_wifi_filter_title">打开 Wi-Fi 过滤器</string> <string name="pref_autodl_wifi_filter_sum">只允许在 Wi-Fi 网络下自动下载</string> - <string name="pref_episode_cache_title">曲目缓存</string> + <string name="pref_episode_cache_title">单集缓存</string> <string name="pref_theme_title_light">浅色</string> <string name="pref_theme_title_dark">暗色</string> <string name="pref_episode_cache_unlimited">无限</string> @@ -210,9 +210,20 @@ <string name="pref_playback_speed_sum">自定义音频播放速度</string> <string name="pref_gpodnet_sethostname_title">设置主机名</string> <string name="pref_gpodnet_sethostname_use_default_host">使用默认主机</string> + <string name="pref_expandNotify_title">扩展通知</string> + <string name="pref_expandNotify_sum">总是扩展通知以显示播放按钮</string> + <string name="pref_persistNotify_title">保持播放控制</string> + <string name="pref_persistNotify_sum">在暂停时保持通知和锁屏界面的控制。</string> + <string name="pref_expand_notify_unsupport_toast">Android 版本 4.1 之前不支持扩展通知</string> + <string name="pref_seek_delta_title">定位时间</string> + <string name="pref_seek_delta_sum">当倒退或快速回放时以这些秒为单位</string> + <string name="pref_auto_delete_sum">当播放完成后删除单集</string> + <string name="pref_auto_delete_title">自动删除</string> + <string name="pref_unpauseOnHeadsetReconnect_title">耳机重新连接</string> + <string name="pref_unpauseOnHeadsetReconnect_sum">当耳机重新连接时恢复播放</string> <!--Auto-Flattr dialog--> <!--Search--> - <string name="search_hint">搜索订阅或者曲目</string> + <string name="search_hint">搜索订阅或者单集</string> <string name="found_in_shownotes_label">笔记中查找</string> <string name="found_in_chapters_label">章节中查找</string> <string name="search_status_no_results">没有找到任何结果</string> @@ -300,17 +311,17 @@ <string name="media_type_video_label">视频</string> <string name="navigate_upwards_label">向上导航</string> <string name="butAction_label">更多动作</string> - <string name="status_playing_label">曲目正在播放</string> - <string name="status_downloading_label">曲目正在下载</string> - <string name="status_downloaded_label">曲目已下载</string> + <string name="status_playing_label">单集正在播放</string> + <string name="status_downloading_label">单集正在下载</string> + <string name="status_downloaded_label">单集已下载</string> <string name="status_unread_label">新项目</string> - <string name="in_queue_label">曲目已经在播放列表中</string> - <string name="new_episodes_count_label">新曲目数</string> - <string name="in_progress_episodes_count_label">已收听曲目数</string> + <string name="in_queue_label">单集已经在播放列表中</string> + <string name="new_episodes_count_label">新单集数</string> + <string name="in_progress_episodes_count_label">已收听单集数</string> <string name="drag_handle_content_description">拖动以变更本项目的位置</string> <!--Feed information screen--> <string name="authentication_label">验证</string> - <string name="authentication_descr">给本播客及曲目变更用户名及密码</string> + <string name="authentication_descr">给本播客及单集变更用户名及密码</string> <!--AntennaPodSP--> <string name="sp_apps_importing_feeds_msg">正在从选定的应用中导入订阅...</string> </resources> diff --git a/core/src/main/res/values/attrs.xml b/core/src/main/res/values/attrs.xml index f36119c8d..368921f76 100644 --- a/core/src/main/res/values/attrs.xml +++ b/core/src/main/res/values/attrs.xml @@ -11,6 +11,7 @@ <attr name="av_rewind" format="reference"/> <attr name="content_discard" format="reference"/> <attr name="content_new" format="reference"/> + <attr name="feed" format="reference"/> <attr name="device_access_time" format="reference"/> <attr name="location_web_site" format="reference"/> <attr name="navigation_accept" format="reference"/> diff --git a/core/src/main/res/values/strings.xml b/core/src/main/res/values/strings.xml index e8c3408b2..72e7a2b31 100644 --- a/core/src/main/res/values/strings.xml +++ b/core/src/main/res/values/strings.xml @@ -67,13 +67,14 @@ <string name="close_label">Close</string> <string name="retry_label">Retry</string> <string name="auto_download_label">Include in auto downloads</string> + <string name="parallel_downloads_suffix">\u0020parallel downloads</string> <!-- 'Add Feed' Activity labels --> <string name="feedurl_label">Feed URL</string> <string name="etxtFeedurlHint">URL of feed or website</string> <string name="txtvfeedurl_label">Add Podcast by URL</string> <string name="podcastdirectories_label">Find podcast in directory</string> - <string name="podcastdirectories_descr">You can search for new podcasts by name, category or popularity in the gpodder.net directory.</string> + <string name="podcastdirectories_descr">You can search for new podcasts by name, category or popularity in the gpodder.net directory, or search the iTunes store.</string> <string name="browse_gpoddernet_label">Browse gpodder.net</string> <!-- Actions on feeds --> @@ -248,6 +249,7 @@ <string name="pref_autodl_wifi_filter_sum">Allow automatic download only for selected Wi-Fi networks.</string> <string name="pref_automatic_download_on_battery_title">Download when not charging</string> <string name="pref_automatic_download_on_battery_sum">Allow automatic download when the battery is not charging</string> + <string name="pref_parallel_downloads_title">Parallel downloads</string> <string name="pref_episode_cache_title">Episode cache</string> <string name="pref_theme_title_light">Light</string> <string name="pref_theme_title_dark">Dark</string> @@ -396,4 +398,5 @@ <!-- AntennaPodSP --> <string name="sp_apps_importing_feeds_msg">Importing subscriptions from single-purpose apps…</string> + <string name="search_itunes_label">Search iTunes</string> </resources> diff --git a/core/src/main/res/values/styles.xml b/core/src/main/res/values/styles.xml index a2f180395..e8b0e2b2b 100644 --- a/core/src/main/res/values/styles.xml +++ b/core/src/main/res/values/styles.xml @@ -15,6 +15,7 @@ <item name="attr/content_discard">@drawable/ic_delete_grey600_24dp</item> <item name="attr/content_new">@drawable/ic_add_grey600_24dp</item> <item name="attr/device_access_time">@drawable/ic_timer_grey600_24dp</item> + <item name="attr/feed">@drawable/ic_feed_grey600_24dp</item> <item name="attr/location_web_site">@drawable/ic_web_grey600_24dp</item> <item name="attr/navigation_accept">@drawable/ic_done_grey600_24dp</item> <item name="attr/navigation_cancel">@drawable/ic_cancel_grey600_24dp</item> @@ -56,6 +57,7 @@ <item name="attr/content_discard">@drawable/ic_delete_white_24dp</item> <item name="attr/content_new">@drawable/ic_add_white_24dp</item> <item name="attr/device_access_time">@drawable/ic_timer_white_24dp</item> + <item name="attr/feed">@drawable/ic_feed_white_24dp</item> <item name="attr/location_web_site">@drawable/ic_web_white_24dp</item> <item name="attr/navigation_accept">@drawable/ic_done_white_24dp</item> <item name="attr/navigation_cancel">@drawable/ic_cancel_white_24dp</item> @@ -100,6 +102,7 @@ <item name="attr/content_discard">@drawable/ic_delete_grey600_24dp</item> <item name="attr/content_new">@drawable/ic_add_grey600_24dp</item> <item name="attr/device_access_time">@drawable/ic_timer_grey600_24dp</item> + <item name="attr/feed">@drawable/ic_feed_grey600_24dp</item> <item name="attr/location_web_site">@drawable/ic_web_grey600_24dp</item> <item name="attr/navigation_accept">@drawable/ic_done_grey600_24dp</item> <item name="attr/navigation_cancel">@drawable/ic_cancel_grey600_24dp</item> @@ -143,6 +146,7 @@ <item name="attr/content_discard">@drawable/ic_delete_white_24dp</item> <item name="attr/content_new">@drawable/ic_add_white_24dp</item> <item name="attr/device_access_time">@drawable/ic_timer_white_24dp</item> + <item name="attr/feed">@drawable/ic_feed_white_24dp</item> <item name="attr/location_web_site">@drawable/ic_web_white_24dp</item> <item name="attr/navigation_accept">@drawable/ic_done_white_24dp</item> <item name="attr/navigation_cancel">@drawable/ic_cancel_white_24dp</item> |