diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/FeedDiscoverAdapter.java b/app/src/main/java/de/danoeh/antennapod/adapter/FeedDiscoverAdapter.java new file mode 100644 index 000000000..df7ec46e0 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/adapter/FeedDiscoverAdapter.java @@ -0,0 +1,76 @@ +package de.danoeh.antennapod.adapter; + +import android.view.View; +import android.view.ViewGroup; +import android.widget.BaseAdapter; +import android.widget.ImageView; +import com.bumptech.glide.Glide; +import com.bumptech.glide.request.RequestOptions; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.activity.MainActivity; +import de.danoeh.antennapod.discovery.PodcastSearchResult; + +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.List; + +public class FeedDiscoverAdapter extends BaseAdapter { + + private final WeakReference mainActivityRef; + private final List data = new ArrayList<>(); + + public FeedDiscoverAdapter(MainActivity mainActivity) { + this.mainActivityRef = new WeakReference<>(mainActivity); + } + + public void updateData(List newData) { + data.clear(); + data.addAll(newData); + notifyDataSetChanged(); + } + + @Override + public int getCount() { + return data.size(); + } + + @Override + public PodcastSearchResult getItem(int position) { + return data.get(position); + } + + @Override + public long getItemId(int position) { + return 0; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + Holder holder; + + if (convertView == null) { + convertView = View.inflate(mainActivityRef.get(), R.layout.quick_feed_discovery_item, null); + holder = new Holder(); + holder.imageView = convertView.findViewById(R.id.discovery_cover); + convertView.setTag(holder); + } else { + holder = (Holder) convertView.getTag(); + } + + + final PodcastSearchResult podcast = getItem(position); + Glide.with(mainActivityRef.get()) + .load(podcast.imageUrl) + .apply(new RequestOptions() + .placeholder(R.color.light_gray) + .fitCenter() + .dontAnimate()) + .into(holder.imageView); + + return convertView; + } + + static class Holder { + ImageView imageView; + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/discovery/ItunesTopListLoader.java b/app/src/main/java/de/danoeh/antennapod/discovery/ItunesTopListLoader.java new file mode 100644 index 000000000..bc9133258 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/discovery/ItunesTopListLoader.java @@ -0,0 +1,110 @@ +package de.danoeh.antennapod.discovery; + +import android.content.Context; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.core.ClientConfig; +import de.danoeh.antennapod.core.service.download.AntennapodHttpClient; +import io.reactivex.Single; +import io.reactivex.SingleOnSubscribe; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.schedulers.Schedulers; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; +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 java.util.Locale; + +public class ItunesTopListLoader { + private final Context context; + + public ItunesTopListLoader(Context context) { + this.context = context; + } + + public Single> loadToplist(int limit) { + return Single.create((SingleOnSubscribe>) emitter -> { + String lang = Locale.getDefault().getLanguage(); + OkHttpClient client = AntennapodHttpClient.getHttpClient(); + String feedString; + try { + try { + feedString = getTopListFeed(client, lang, limit); + } catch (IOException e) { + feedString = getTopListFeed(client, "us", limit); + } + List podcasts = parseFeed(feedString); + emitter.onSuccess(podcasts); + } catch (IOException | JSONException e) { + emitter.onError(e); + } + }) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()); + } + + public Single getFeedUrl(PodcastSearchResult podcast) { + if (!podcast.feedUrl.contains("itunes.apple.com")) { + return Single.just(podcast.feedUrl) + .observeOn(AndroidSchedulers.mainThread()); + } + return Single.create((SingleOnSubscribe) emitter -> { + OkHttpClient client = AntennapodHttpClient.getHttpClient(); + Request.Builder httpReq = new Request.Builder() + .url(podcast.feedUrl) + .header("User-Agent", ClientConfig.USER_AGENT); + try { + Response response = client.newCall(httpReq.build()).execute(); + if (response.isSuccessful()) { + String resultString = response.body().string(); + JSONObject result = new JSONObject(resultString); + JSONObject results = result.getJSONArray("results").getJSONObject(0); + String feedUrl = results.getString("feedUrl"); + emitter.onSuccess(feedUrl); + } else { + String prefix = context.getString(R.string.error_msg_prefix); + emitter.onError(new IOException(prefix + response)); + } + } catch (IOException | JSONException e) { + emitter.onError(e); + } + }) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()); + } + + private String getTopListFeed(OkHttpClient client, String language, int limit) throws IOException { + String url = "https://itunes.apple.com/%s/rss/toppodcasts/limit="+limit+"/explicit=true/json"; + Request.Builder httpReq = new Request.Builder() + .header("User-Agent", ClientConfig.USER_AGENT) + .url(String.format(url, language)); + + try (Response response = client.newCall(httpReq.build()).execute()) { + if (response.isSuccessful()) { + return response.body().string(); + } + String prefix = context.getString(R.string.error_msg_prefix); + throw new IOException(prefix + response); + } + } + + private List parseFeed(String jsonString) throws JSONException { + JSONObject result = new JSONObject(jsonString); + JSONObject feed = result.getJSONObject("feed"); + JSONArray entries = feed.getJSONArray("entry"); + + List results = new ArrayList<>(); + for (int i=0; i < entries.length(); i++) { + JSONObject json = entries.getJSONObject(i); + results.add(PodcastSearchResult.fromItunesToplist(json)); + } + + return results; + } + +} diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/ItunesSearchFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/ItunesSearchFragment.java index f1b10158d..80767bef2 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/ItunesSearchFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/ItunesSearchFragment.java @@ -21,6 +21,7 @@ import android.widget.TextView; import com.afollestad.materialdialogs.MaterialDialog; import de.danoeh.antennapod.discovery.ItunesPodcastSearcher; +import de.danoeh.antennapod.discovery.ItunesTopListLoader; import de.danoeh.antennapod.discovery.PodcastSearchResult; import org.json.JSONArray; import org.json.JSONException; @@ -114,60 +115,30 @@ public class ItunesSearchFragment extends Fragment { //Show information about the podcast when the list item is clicked gridView.setOnItemClickListener((parent, view1, position, id) -> { PodcastSearchResult podcast = searchResults.get(position); - if(podcast.feedUrl == null) { + if (podcast.feedUrl == null) { return; } - if (!podcast.feedUrl.contains("itunes.apple.com")) { - Intent intent = new Intent(getActivity(), OnlineFeedViewActivity.class); - intent.putExtra(OnlineFeedViewActivity.ARG_FEEDURL, podcast.feedUrl); - intent.putExtra(OnlineFeedViewActivity.ARG_TITLE, "iTunes"); - startActivity(intent); - } else { - gridView.setVisibility(View.GONE); - progressBar.setVisibility(View.VISIBLE); - disposable = Single.create((SingleOnSubscribe) emitter -> { - OkHttpClient client = AntennapodHttpClient.getHttpClient(); - Request.Builder httpReq = new Request.Builder() - .url(podcast.feedUrl) - .header("User-Agent", ClientConfig.USER_AGENT); - try { - Response response = client.newCall(httpReq.build()).execute(); - if (response.isSuccessful()) { - String resultString = response.body().string(); - JSONObject result = new JSONObject(resultString); - JSONObject results = result.getJSONArray("results").getJSONObject(0); - String feedUrl = results.getString("feedUrl"); - emitter.onSuccess(feedUrl); - } else { - String prefix = getString(R.string.error_msg_prefix); - emitter.onError(new IOException(prefix + response)); - } - } catch (IOException | JSONException e) { - if (!disposable.isDisposed()) { - emitter.onError(e); - } - } - }) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(feedUrl -> { - progressBar.setVisibility(View.GONE); - gridView.setVisibility(View.VISIBLE); - Intent intent = new Intent(getActivity(), OnlineFeedViewActivity.class); - intent.putExtra(OnlineFeedViewActivity.ARG_FEEDURL, feedUrl); - intent.putExtra(OnlineFeedViewActivity.ARG_TITLE, "iTunes"); - startActivity(intent); - }, error -> { - Log.e(TAG, Log.getStackTraceString(error)); - progressBar.setVisibility(View.GONE); - gridView.setVisibility(View.VISIBLE); - String prefix = getString(R.string.error_msg_prefix); - new MaterialDialog.Builder(getActivity()) - .content(prefix + " " + error.getMessage()) - .neutralText(android.R.string.ok) - .show(); - }); - } + gridView.setVisibility(View.GONE); + progressBar.setVisibility(View.VISIBLE); + ItunesTopListLoader loader = new ItunesTopListLoader(getContext()); + disposable = loader.getFeedUrl(podcast) + .subscribe(feedUrl -> { + progressBar.setVisibility(View.GONE); + gridView.setVisibility(View.VISIBLE); + Intent intent = new Intent(getActivity(), OnlineFeedViewActivity.class); + intent.putExtra(OnlineFeedViewActivity.ARG_FEEDURL, feedUrl); + intent.putExtra(OnlineFeedViewActivity.ARG_TITLE, "iTunes"); + startActivity(intent); + }, error -> { + Log.e(TAG, Log.getStackTraceString(error)); + progressBar.setVisibility(View.GONE); + gridView.setVisibility(View.VISIBLE); + String prefix = getString(R.string.error_msg_prefix); + new MaterialDialog.Builder(getActivity()) + .content(prefix + " " + error.getMessage()) + .neutralText(android.R.string.ok) + .show(); + }); }); progressBar = root.findViewById(R.id.progressBar); txtvError = root.findViewById(R.id.txtvError); @@ -235,26 +206,9 @@ public class ItunesSearchFragment extends Fragment { butRetry.setVisibility(View.GONE); txtvEmpty.setVisibility(View.GONE); progressBar.setVisibility(View.VISIBLE); - disposable = Single.create((SingleOnSubscribe>) emitter -> { - String lang = Locale.getDefault().getLanguage(); - OkHttpClient client = AntennapodHttpClient.getHttpClient(); - String feedString; - try { - try { - feedString = getTopListFeed(client, lang); - } catch (IOException e) { - feedString = getTopListFeed(client, "us"); - } - List podcasts = parseFeed(feedString); - emitter.onSuccess(podcasts); - } catch (IOException | JSONException e) { - if (!disposable.isDisposed()) { - emitter.onError(e); - } - } - }) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) + + ItunesTopListLoader loader = new ItunesTopListLoader(getContext()); + disposable = loader.loadToplist(25) .subscribe(podcasts -> { progressBar.setVisibility(View.GONE); topList = podcasts; @@ -269,35 +223,6 @@ public class ItunesSearchFragment extends Fragment { }); } - private String getTopListFeed(OkHttpClient client, String language) throws IOException { - String url = "https://itunes.apple.com/%s/rss/toppodcasts/limit=25/explicit=true/json"; - Request.Builder httpReq = new Request.Builder() - .header("User-Agent", ClientConfig.USER_AGENT) - .url(String.format(url, language)); - - try (Response response = client.newCall(httpReq.build()).execute()) { - if (response.isSuccessful()) { - return response.body().string(); - } - String prefix = getString(R.string.error_msg_prefix); - throw new IOException(prefix + response); - } - } - - private List parseFeed(String jsonString) throws JSONException { - JSONObject result = new JSONObject(jsonString); - JSONObject feed = result.getJSONObject("feed"); - JSONArray entries = feed.getJSONArray("entry"); - - List results = new ArrayList<>(); - for (int i=0; i < entries.length(); i++) { - JSONObject json = entries.getJSONObject(i); - results.add(PodcastSearchResult.fromItunesToplist(json)); - } - - return results; - } - private void search(String query) { if (disposable != null) { disposable.dispose(); diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/QuickFeedDiscoveryFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/QuickFeedDiscoveryFragment.java new file mode 100644 index 000000000..6b3168ee6 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/fragment/QuickFeedDiscoveryFragment.java @@ -0,0 +1,100 @@ +package de.danoeh.antennapod.fragment; + +import android.content.Intent; +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.GridView; +import android.widget.ProgressBar; +import com.afollestad.materialdialogs.MaterialDialog; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.activity.MainActivity; +import de.danoeh.antennapod.activity.OnlineFeedViewActivity; +import de.danoeh.antennapod.adapter.FeedDiscoverAdapter; +import de.danoeh.antennapod.discovery.ItunesTopListLoader; +import de.danoeh.antennapod.discovery.PodcastSearchResult; +import io.reactivex.disposables.Disposable; + + +public class QuickFeedDiscoveryFragment extends Fragment implements AdapterView.OnItemClickListener { + private static final String TAG = "FeedDiscoveryFragment"; + + private ProgressBar progressBar; + private Disposable disposable; + private FeedDiscoverAdapter adapter; + private GridView subscriptionGridLayout; + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + super.onCreateView(inflater, container, savedInstanceState); + View root = inflater.inflate(R.layout.quick_feed_discovery, container, false); + View discoverMore = root.findViewById(R.id.discover_more); + discoverMore.setOnClickListener(v -> + ((MainActivity) getActivity()).loadChildFragment(new ItunesSearchFragment())); + subscriptionGridLayout = root.findViewById(R.id.discover_grid); + + progressBar = root.findViewById(R.id.discover_progress_bar); + adapter = new FeedDiscoverAdapter((MainActivity) getActivity()); + subscriptionGridLayout.setAdapter(adapter); + subscriptionGridLayout.setOnItemClickListener(this); + + loadToplist(); + + return root; + } + + @Override + public void onDestroy() { + super.onDestroy(); + if (disposable != null) { + disposable.dispose(); + } + } + + private void loadToplist() { + progressBar.setVisibility(View.VISIBLE); + subscriptionGridLayout.setVisibility(View.GONE); + + ItunesTopListLoader loader = new ItunesTopListLoader(getContext()); + disposable = loader.loadToplist(8) + .subscribe(podcasts -> { + progressBar.setVisibility(View.GONE); + subscriptionGridLayout.setVisibility(View.VISIBLE); + adapter.updateData(podcasts); + }, error -> { + Log.e(TAG, Log.getStackTraceString(error)); + progressBar.setVisibility(View.GONE); + subscriptionGridLayout.setVisibility(View.VISIBLE); + }); + } + + @Override + public void onItemClick(AdapterView parent, final View view, int position, long id) { + PodcastSearchResult podcast = adapter.getItem(position); + if (podcast.feedUrl == null) { + return; + } + view.setAlpha(0.5f); + ItunesTopListLoader loader = new ItunesTopListLoader(getContext()); + disposable = loader.getFeedUrl(podcast) + .subscribe(feedUrl -> { + view.setAlpha(1f); + Intent intent = new Intent(getActivity(), OnlineFeedViewActivity.class); + intent.putExtra(OnlineFeedViewActivity.ARG_FEEDURL, feedUrl); + intent.putExtra(OnlineFeedViewActivity.ARG_TITLE, getString(R.string.add_feed_label)); + startActivity(intent); + }, error -> { + Log.e(TAG, Log.getStackTraceString(error)); + view.setAlpha(1f); + String prefix = getString(R.string.error_msg_prefix); + new MaterialDialog.Builder(getActivity()) + .content(prefix + " " + error.getMessage()) + .neutralText(android.R.string.ok) + .show(); + }); + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/view/WrappingGridView.java b/app/src/main/java/de/danoeh/antennapod/view/WrappingGridView.java new file mode 100644 index 000000000..d155df9d2 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/view/WrappingGridView.java @@ -0,0 +1,36 @@ +package de.danoeh.antennapod.view; + +import android.content.Context; +import android.util.AttributeSet; +import android.widget.GridView; + +/** + * Source: https://stackoverflow.com/a/46350213/ + */ +public class WrappingGridView extends GridView { + + public WrappingGridView(Context context) { + super(context); + } + + public WrappingGridView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public WrappingGridView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int heightSpec = heightMeasureSpec; + if (getLayoutParams().height == LayoutParams.WRAP_CONTENT) { + // The great Android "hackatlon", the love, the magic. + // The two leftmost bits in the height measure spec have + // a special meaning, hence we can't use them to describe height. + heightSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2, MeasureSpec.AT_MOST); + } + super.onMeasure(widthMeasureSpec, heightSpec); + } + +} \ No newline at end of file diff --git a/app/src/main/res/layout/addfeed.xml b/app/src/main/res/layout/addfeed.xml index f4f5541f9..c00aa6924 100644 --- a/app/src/main/res/layout/addfeed.xml +++ b/app/src/main/res/layout/addfeed.xml @@ -1,8 +1,7 @@ + xmlns:app="http://schemas.android.com/apk/res-auto" > + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/quick_feed_discovery_item.xml b/app/src/main/res/layout/quick_feed_discovery_item.xml new file mode 100644 index 000000000..6b0c98013 --- /dev/null +++ b/app/src/main/res/layout/quick_feed_discovery_item.xml @@ -0,0 +1,14 @@ + + + + + + + diff --git a/core/src/main/res/values/strings.xml b/core/src/main/res/values/strings.xml index 934468327..6025dc888 100644 --- a/core/src/main/res/values/strings.xml +++ b/core/src/main/res/values/strings.xml @@ -122,6 +122,8 @@ Find Podcast in Directory For new podcasts, you can search iTunes or fyyd, or browse gpodder.net by name, category or popularity. Browse gpodder.net + Discover + more ยป Mark all as played