From 5a077774fcfeb89fc3eac6993c4320d81c5ed157 Mon Sep 17 00:00:00 2001 From: Martin Fietz Date: Wed, 20 Jan 2016 19:10:51 +0100 Subject: [PATCH] iTunes Search: SearchView in Action Bar, error/result message, retry, feed url --- .../adapter/itunes/ItunesAdapter.java | 51 +++- .../fragment/ItunesSearchFragment.java | 247 ++++++++++++++---- .../res/layout/fragment_itunes_search.xml | 86 ++++-- .../res/layout/itunes_podcast_listitem.xml | 86 +++--- app/src/main/res/menu/itunes_search.xml | 13 + core/src/main/res/values/styles.xml | 6 + 6 files changed, 380 insertions(+), 109 deletions(-) create mode 100644 app/src/main/res/menu/itunes_search.xml 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 index 08ffdd197..47ac4c757 100644 --- a/app/src/main/java/de/danoeh/antennapod/adapter/itunes/ItunesAdapter.java +++ b/app/src/main/java/de/danoeh/antennapod/adapter/itunes/ItunesAdapter.java @@ -10,6 +10,7 @@ import android.widget.TextView; import com.bumptech.glide.Glide; import com.bumptech.glide.load.engine.DiskCacheStrategy; +import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; @@ -65,6 +66,12 @@ public class ItunesAdapter extends ArrayAdapter { //Set the title viewHolder.titleView.setText(podcast.title); + if(!podcast.feedUrl.contains("itunes.apple.com")) { + viewHolder.urlView.setText(podcast.feedUrl); + viewHolder.urlView.setVisibility(View.VISIBLE); + } else { + viewHolder.urlView.setVisibility(View.GONE); + } //Update the empty imageView with the image from the feed Glide.with(context) @@ -94,6 +101,8 @@ public class ItunesAdapter extends ArrayAdapter { */ public final TextView titleView; + public final TextView urlView; + /** * Constructor @@ -102,6 +111,7 @@ public class ItunesAdapter extends ArrayAdapter { PodcastViewHolder(View view){ coverView = (ImageView) view.findViewById(R.id.imgvCover); titleView = (TextView) view.findViewById(R.id.txtvTitle); + urlView = (TextView) view.findViewById(R.id.txtvUrl); } } @@ -124,16 +134,47 @@ public class ItunesAdapter extends ArrayAdapter { */ public final String feedUrl; + + private Podcast(String title, String imageUrl, String feedUrl) { + this.title = title; + this.imageUrl = imageUrl; + this.feedUrl = feedUrl; + } + /** - * Constructor. + * Constructs a Podcast instance from a iTunes search result * * @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"); + public static Podcast fromSearch(JSONObject json) throws JSONException { + String title = json.getString("collectionName"); + String imageUrl = json.getString("artworkUrl100"); + String feedUrl = json.getString("feedUrl"); + return new Podcast(title, imageUrl, feedUrl); } + + /** + * Constructs a Podcast instance from iTunes toplist entry + * + * @param json object holding the podcast information + * @throws JSONException + */ + public static Podcast fromToplist(JSONObject json) throws JSONException { + String title = json.getJSONObject("title").getString("label"); + String imageUrl = null; + JSONArray images = json.getJSONArray("im:image"); + for(int i=0; imageUrl == null && i < images.length(); i++) { + JSONObject image = images.getJSONObject(i); + String height = image.getJSONObject("attributes").getString("height"); + if(Integer.valueOf(height) >= 100) { + imageUrl = image.getString("label"); + } + } + String feedUrl = "https://itunes.apple.com/lookup?id=" + + json.getJSONObject("id").getJSONObject("attributes").getString("im:id"); + return new Podcast(title, imageUrl, feedUrl); + } + } } 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 e92df4885..eb947dc2b 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/ItunesSearchFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/ItunesSearchFragment.java @@ -1,17 +1,23 @@ package de.danoeh.antennapod.fragment; import android.content.Intent; -import android.content.res.Resources; import android.os.Bundle; import android.support.v4.app.Fragment; +import android.support.v4.view.MenuItemCompat; import android.support.v7.widget.SearchView; import android.util.Log; import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; -import android.widget.AdapterView; +import android.widget.Button; import android.widget.GridView; +import android.widget.ProgressBar; +import android.widget.TextView; +import com.afollestad.materialdialogs.MaterialDialog; import com.squareup.okhttp.OkHttpClient; import com.squareup.okhttp.Request; import com.squareup.okhttp.Response; @@ -25,13 +31,14 @@ import java.io.UnsupportedEncodingException; import java.net.URLEncoder; import java.util.ArrayList; import java.util.List; +import java.util.Locale; import de.danoeh.antennapod.R; import de.danoeh.antennapod.activity.OnlineFeedViewActivity; import de.danoeh.antennapod.adapter.itunes.ItunesAdapter; import de.danoeh.antennapod.core.ClientConfig; -import de.danoeh.antennapod.core.preferences.UserPreferences; import de.danoeh.antennapod.core.service.download.AntennapodHttpClient; +import de.danoeh.antennapod.menuhandler.MenuItemUtils; import rx.Observable; import rx.Subscription; import rx.android.schedulers.AndroidSchedulers; @@ -46,21 +53,22 @@ public class ItunesSearchFragment extends Fragment { private static final String API_URL = "https://itunes.apple.com/search?media=podcast&term=%s"; - /** - * Search input field - */ - private SearchView searchView; /** * Adapter responsible with the search results */ private ItunesAdapter adapter; + private GridView gridView; + private ProgressBar progressBar; + private TextView txtvError; + private Button butRetry; + private TextView txtvEmpty; /** * List of podcasts retreived from the search */ private List searchResults; - + private List topList; private Subscription subscription; /** @@ -70,13 +78,17 @@ public class ItunesSearchFragment extends Fragment { void updateData(List result) { this.searchResults = result; adapter.clear(); - - //ArrayAdapter.addAll() requires minsdk > 10 - for(Podcast p: result) { - adapter.add(p); + if (result != null && result.size() > 0) { + gridView.setVisibility(View.VISIBLE); + txtvEmpty.setVisibility(View.GONE); + for (Podcast p : result) { + adapter.add(p); + } + adapter.notifyDataSetInvalidated(); + } else { + gridView.setVisibility(View.GONE); + txtvEmpty.setVisibility(View.VISIBLE); } - - adapter.notifyDataSetInvalidated(); } /** @@ -89,47 +101,105 @@ public class ItunesSearchFragment extends Fragment { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - adapter = new ItunesAdapter(getActivity(), new ArrayList()); - + setHasOptionsMenu(true); } @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); + View root = inflater.inflate(R.layout.fragment_itunes_search, container, false); + gridView = (GridView) root.findViewById(R.id.gridView); + adapter = new ItunesAdapter(getActivity(), new ArrayList<>()); 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(), - OnlineFeedViewActivity.class); - - //Tell the OnlineFeedViewActivity where to go - String url = searchResults.get(position).feedUrl; - intent.putExtra(OnlineFeedViewActivity.ARG_FEEDURL, url); - + gridView.setOnItemClickListener((parent, view1, position, id) -> { + Podcast podcast = searchResults.get(position); + 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); + rx.Observable.create((Observable.OnSubscribe) subscriber -> { + 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"); + subscriber.onNext(feedUrl); + } else { + String prefix = getString(R.string.error_msg_prefix); + subscriber.onError(new IOException(prefix + response)); + } + } catch (IOException | JSONException e) { + subscriber.onError(e); + } + subscriber.onCompleted(); + }) + .subscribeOn(Schedulers.newThread()) + .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(); + }); } }); + progressBar = (ProgressBar) root.findViewById(R.id.progressBar); + txtvError = (TextView) root.findViewById(R.id.txtvError); + butRetry = (Button) root.findViewById(R.id.butRetry); + txtvEmpty = (TextView) root.findViewById(android.R.id.empty); - //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() { + loadToplist(); + + return root; + } + + @Override + public void onDestroy() { + super.onDestroy(); + if (subscription != null) { + subscription.unsubscribe(); + } + adapter = null; + } + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + super.onCreateOptionsMenu(menu, inflater); + inflater.inflate(R.menu.itunes_search, menu); + MenuItem searchItem = menu.findItem(R.id.action_search); + final SearchView sv = (SearchView) MenuItemCompat.getActionView(searchItem); + MenuItemUtils.adjustTextColor(getActivity(), sv); + sv.setQueryHint(getString(R.string.search_itunes_label)); + sv.setOnQueryTextListener(new android.support.v7.widget.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(); + sv.clearFocus(); search(s); - return false; + return true; } @Override @@ -137,21 +207,97 @@ public class ItunesSearchFragment extends Fragment { return false; } }); + MenuItemCompat.setOnActionExpandListener(searchItem, new MenuItemCompat.OnActionExpandListener() { + @Override + public boolean onMenuItemActionExpand(MenuItem item) { + return true; + } - SearchView.SearchAutoComplete textField = (SearchView.SearchAutoComplete) searchView.findViewById(de.danoeh.antennapod.R.id.search_src_text); - if(UserPreferences.getTheme() == de.danoeh.antennapod.R.style.Theme_AntennaPod_Dark) { - textField.setTextColor(Resources.getSystem().getColor(android.R.color.white)); - } else { - textField.setTextColor(Resources.getSystem().getColor(android.R.color.black)); + @Override + public boolean onMenuItemActionCollapse(MenuItem item) { + if(searchResults != null) { + searchResults = null; + updateData(topList); + } + return true; + } + }); + } + + private void loadToplist() { + if (subscription != null) { + subscription.unsubscribe(); } + gridView.setVisibility(View.GONE); + txtvError.setVisibility(View.GONE); + butRetry.setVisibility(View.GONE); + txtvEmpty.setVisibility(View.GONE); + progressBar.setVisibility(View.VISIBLE); + subscription = rx.Observable.create((Observable.OnSubscribe>) subscriber -> { + String lang = Locale.getDefault().getLanguage(); + String url = "https://itunes.apple.com/" + lang + "/rss/toppodcasts/limit=25/explicit=true/json"; + OkHttpClient client = AntennapodHttpClient.getHttpClient(); + Request.Builder httpReq = new Request.Builder() + .url(url) + .header("User-Agent", ClientConfig.USER_AGENT); + List results = new ArrayList<>(); + try { + Response response = client.newCall(httpReq.build()).execute(); + if(!response.isSuccessful()) { + // toplist for language does not exist, fall back to united states + url = "https://itunes.apple.com/us/rss/toppodcasts/limit=25/explicit=true/json"; + httpReq = new Request.Builder() + .url(url) + .header("User-Agent", ClientConfig.USER_AGENT); + response = client.newCall(httpReq.build()).execute(); + } + if(response.isSuccessful()) { + String resultString = response.body().string(); + JSONObject result = new JSONObject(resultString); + JSONObject feed = result.getJSONObject("feed"); + JSONArray entries = feed.getJSONArray("entry"); - return view; + for(int i=0; i < entries.length(); i++) { + JSONObject json = entries.getJSONObject(i); + Podcast podcast = Podcast.fromToplist(json); + results.add(podcast); + } + } + else { + String prefix = getString(R.string.error_msg_prefix); + subscriber.onError(new IOException(prefix + response)); + } + } catch (IOException | JSONException e) { + subscriber.onError(e); + } + subscriber.onNext(results); + subscriber.onCompleted(); + }) + .subscribeOn(Schedulers.newThread()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(podcasts -> { + progressBar.setVisibility(View.GONE); + topList = podcasts; + updateData(topList); + }, error -> { + Log.e(TAG, Log.getStackTraceString(error)); + progressBar.setVisibility(View.GONE); + txtvError.setText(error.toString()); + txtvError.setVisibility(View.VISIBLE); + butRetry.setOnClickListener(v -> loadToplist()); + butRetry.setVisibility(View.VISIBLE); + }); } private void search(String query) { if (subscription != null) { subscription.unsubscribe(); } + gridView.setVisibility(View.GONE); + txtvError.setVisibility(View.GONE); + butRetry.setVisibility(View.GONE); + txtvEmpty.setVisibility(View.GONE); + progressBar.setVisibility(View.VISIBLE); subscription = rx.Observable.create((Observable.OnSubscribe>) subscriber -> { String encodedQuery = null; try { @@ -181,15 +327,16 @@ public class ItunesSearchFragment extends Fragment { for (int i = 0; i < j.length(); i++) { JSONObject podcastJson = j.getJSONObject(i); - Podcast podcast = new Podcast(podcastJson); + Podcast podcast = Podcast.fromSearch(podcastJson); podcasts.add(podcast); } } else { - subscriber.onError(new IOException("Unexpected error: " + response)); + String prefix = getString(R.string.error_msg_prefix); + subscriber.onError(new IOException(prefix + response)); } } catch (IOException | JSONException e) { - Log.e(TAG, Log.getStackTraceString(e)); + subscriber.onError(e); } subscriber.onNext(podcasts); subscriber.onCompleted(); @@ -197,9 +344,15 @@ public class ItunesSearchFragment extends Fragment { .subscribeOn(Schedulers.newThread()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(podcasts -> { + progressBar.setVisibility(View.GONE); updateData(podcasts); }, error -> { Log.e(TAG, Log.getStackTraceString(error)); + progressBar.setVisibility(View.GONE); + txtvError.setText(error.toString()); + txtvError.setVisibility(View.VISIBLE); + butRetry.setOnClickListener(v -> search(query)); + butRetry.setVisibility(View.VISIBLE); }); } diff --git a/app/src/main/res/layout/fragment_itunes_search.xml b/app/src/main/res/layout/fragment_itunes_search.xml index e57c59554..0cc13f74c 100644 --- a/app/src/main/res/layout/fragment_itunes_search.xml +++ b/app/src/main/res/layout/fragment_itunes_search.xml @@ -1,26 +1,64 @@ - - - + android:layout_height="match_parent"> + + + + + + + + + +