iTunes Search: SearchView in Action Bar, error/result message, retry, feed url

This commit is contained in:
Martin Fietz 2016-01-20 19:10:51 +01:00
parent e65d6bddf3
commit 5a077774fc
6 changed files with 380 additions and 109 deletions

View File

@ -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<ItunesAdapter.Podcast> {
//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<ItunesAdapter.Podcast> {
*/
public final TextView titleView;
public final TextView urlView;
/**
* Constructor
@ -102,6 +111,7 @@ public class ItunesAdapter extends ArrayAdapter<ItunesAdapter.Podcast> {
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<ItunesAdapter.Podcast> {
*/
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);
}
}
}

View File

@ -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<Podcast> searchResults;
private List<Podcast> topList;
private Subscription subscription;
/**
@ -70,13 +78,17 @@ public class ItunesSearchFragment extends Fragment {
void updateData(List<Podcast> 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<Podcast>());
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<String>) 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<List<Podcast>>) 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<Podcast> 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<List<Podcast>>) 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);
});
}

View File

@ -1,26 +1,64 @@
<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"
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
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="400dp"
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" />
android:layout_height="match_parent">
<GridView
android:id="@+id/gridView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:columnWidth="400dp"
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" />
<TextView
android:id="@id/android:empty"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_centerInParent="true"
android:gravity="center"
android:visibility="gone"
android:text="@string/search_status_no_results" />
<ProgressBar
android:id="@+id/progressBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:indeterminateOnly="true"
android:visibility="gone"/>
<TextView
android:id="@+id/txtvError"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:layout_margin="16dp"
android:textAlignment="center"
android:textSize="@dimen/text_size_small"
android:visibility="gone"
tools:visibility="visible"
tools:text="Error message"
tools:background="@android:color/holo_red_light" />
<Button
android:id="@+id/butRetry"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/txtvError"
android:layout_centerHorizontal="true"
android:layout_margin="16dp"
android:text="@string/retry_label"
android:visibility="gone"
tools:visibility="visible"
tools:background="@android:color/holo_red_light" />
</RelativeLayout>

View File

@ -1,38 +1,58 @@
<?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"
<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: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" />
android:paddingTop="8dp"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:paddingBottom="8dp"
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_alignParentTop="true"
android:layout_marginRight="8dp"
android:adjustViewBounds="true"
android:contentDescription="@string/cover_label"
android:cropToPadding="true"
android:scaleType="fitXY"
tools:background="@android:color/holo_green_dark"
tools:src="@drawable/ic_stat_antenna_default" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_toRightOf="@id/imgvCover"
android:layout_centerVertical="true"
android:orientation="vertical">
<TextView
android:id="@+id/txtvTitle"
style="@style/AntennaPod.TextView.ListItemPrimaryTitle2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:maxLines="2"
tools:background="@android:color/holo_green_dark"
tools:text="Podcast title" />
<TextView
android:id="@+id/txtvUrl"
style="android:style/TextAppearance.Small"
android:layout_width="match_parent"
android:layout_height="wrap_content"
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"/>
</LinearLayout>
</RelativeLayout>

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:custom="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_search"
android:icon="?attr/action_search"
custom:showAsAction="collapseActionView|ifRoom"
custom:actionViewClass="android.support.v7.widget.SearchView"
android:title="@string/search_label"/>
</menu>

View File

@ -249,6 +249,12 @@
<item name="android:ellipsize">end</item>
</style>
<style name="AntennaPod.TextView.ListItemPrimaryTitle2" parent="@android:style/TextAppearance.Small">
<item name="android:textSize">16sp</item>
<item name="android:textColor">?android:attr/textColorPrimary</item>
<item name="android:ellipsize">end</item>
</style>
<style name="AntennaPod.TextView.ListItemSecondaryTitle" parent="@android:style/TextAppearance.Small">
<item name="android:textSize">14sp</item>
<item name="android:textColor">?android:attr/textColorSecondary</item>