| package de.danoeh.antennapod.activity; |
| |
| import android.app.Dialog; |
| import android.content.Context; |
| import android.content.DialogInterface; |
| import android.content.Intent; |
| import android.os.Bundle; |
| import android.os.Looper; |
| import android.support.v4.app.NavUtils; |
| import android.support.v7.app.ActionBarActivity; |
| import android.support.v7.app.AlertDialog; |
| import android.text.TextUtils; |
| import android.util.Log; |
| import android.view.LayoutInflater; |
| import android.view.MenuItem; |
| import android.view.View; |
| import android.widget.AdapterView; |
| import android.widget.ArrayAdapter; |
| import android.widget.Button; |
| import android.widget.ImageView; |
| import android.widget.LinearLayout; |
| import android.widget.ListView; |
| import android.widget.ProgressBar; |
| import android.widget.RelativeLayout; |
| import android.widget.Spinner; |
| import android.widget.TextView; |
| |
| import com.bumptech.glide.Glide; |
| |
| import org.apache.commons.lang3.StringUtils; |
| import org.jsoup.Jsoup; |
| import org.jsoup.examples.HtmlToPlainText; |
| import org.jsoup.nodes.Document; |
| |
| import java.io.File; |
| import java.io.IOException; |
| import java.util.ArrayList; |
| import java.util.List; |
| import java.util.Map; |
| |
| import de.danoeh.antennapod.R; |
| import de.danoeh.antennapod.adapter.FeedItemlistDescriptionAdapter; |
| import de.danoeh.antennapod.core.dialog.DownloadRequestErrorDialogCreator; |
| import de.danoeh.antennapod.core.event.DownloadEvent; |
| import de.danoeh.antennapod.core.feed.EventDistributor; |
| import de.danoeh.antennapod.core.feed.Feed; |
| import de.danoeh.antennapod.core.feed.FeedItem; |
| import de.danoeh.antennapod.core.feed.FeedPreferences; |
| import de.danoeh.antennapod.core.glide.ApGlideSettings; |
| import de.danoeh.antennapod.core.preferences.UserPreferences; |
| import de.danoeh.antennapod.core.service.download.DownloadRequest; |
| import de.danoeh.antennapod.core.service.download.DownloadStatus; |
| import de.danoeh.antennapod.core.service.download.Downloader; |
| import de.danoeh.antennapod.core.service.download.HttpDownloader; |
| import de.danoeh.antennapod.core.storage.DBReader; |
| import de.danoeh.antennapod.core.storage.DownloadRequestException; |
| import de.danoeh.antennapod.core.storage.DownloadRequester; |
| import de.danoeh.antennapod.core.syndication.handler.FeedHandler; |
| import de.danoeh.antennapod.core.syndication.handler.FeedHandlerResult; |
| import de.danoeh.antennapod.core.syndication.handler.UnsupportedFeedtypeException; |
| import de.danoeh.antennapod.core.util.DownloadError; |
| import de.danoeh.antennapod.core.util.FileNameGenerator; |
| import de.danoeh.antennapod.core.util.StorageUtils; |
| import de.danoeh.antennapod.core.util.URLChecker; |
| import de.danoeh.antennapod.core.util.syndication.FeedDiscoverer; |
| import de.danoeh.antennapod.dialog.AuthenticationDialog; |
| import de.greenrobot.event.EventBus; |
| import rx.Observable; |
| import rx.Subscriber; |
| import rx.Subscription; |
| import rx.android.schedulers.AndroidSchedulers; |
| import rx.schedulers.Schedulers; |
| |
| /** |
| * Downloads a feed from a feed URL and parses it. Subclasses can display the |
| * feed object that was parsed. This activity MUST be started with a given URL |
| * or an Exception will be thrown. |
| * <p/> |
| * If the feed cannot be downloaded or parsed, an error dialog will be displayed |
| * and the activity will finish as soon as the error dialog is closed. |
| */ |
| public class OnlineFeedViewActivity extends ActionBarActivity { |
| |
| private static final String TAG = "OnlineFeedViewActivity"; |
| |
| public static final String ARG_FEEDURL = "arg.feedurl"; |
| |
| // Optional argument: specify a title for the actionbar. |
| public static final String ARG_TITLE = "title"; |
| |
| private static final int EVENTS = EventDistributor.FEED_LIST_UPDATE; |
| |
| public static final int RESULT_ERROR = 2; |
| |
| private volatile List<Feed> feeds; |
| private Feed feed; |
| private String selectedDownloadUrl; |
| private Downloader downloader; |
| |
| private boolean isPaused; |
| |
| private Dialog dialog; |
| |
| private Button subscribeButton; |
| |
| private Subscription download; |
| private Subscription parser; |
| private Subscription updater; |
| |
| public void onEventMainThread(DownloadEvent event) { |
| Log.d(TAG, "onEventMainThread() called with: " + "event = [" + event + "]"); |
| setSubscribeButtonState(feed); |
| } |
| |
| private EventDistributor.EventListener listener = new EventDistributor.EventListener() { |
| @Override |
| public void update(EventDistributor eventDistributor, Integer arg) { |
| if ((arg & EventDistributor.FEED_LIST_UPDATE) != 0) { |
| updater = Observable.fromCallable(() -> DBReader.getFeedList()) |
| .subscribeOn(Schedulers.newThread()) |
| .observeOn(AndroidSchedulers.mainThread()) |
| .subscribe(feeds -> { |
| OnlineFeedViewActivity.this.feeds = feeds; |
| setSubscribeButtonState(feed); |
| } |
| ); |
| } else if ((arg & EVENTS) != 0) { |
| setSubscribeButtonState(feed); |
| } |
| } |
| }; |
| |
| @Override |
| protected void onCreate(Bundle savedInstanceState) { |
| setTheme(UserPreferences.getTheme()); |
| super.onCreate(savedInstanceState); |
| getSupportActionBar().setDisplayHomeAsUpEnabled(true); |
| |
| if (getIntent() != null && getIntent().hasExtra(ARG_TITLE)) { |
| getSupportActionBar().setTitle(getIntent().getStringExtra(ARG_TITLE)); |
| } |
| |
| StorageUtils.checkStorageAvailability(this); |
| |
| final String feedUrl; |
| if (getIntent().hasExtra(ARG_FEEDURL)) { |
| feedUrl = getIntent().getStringExtra(ARG_FEEDURL); |
| } else if (TextUtils.equals(getIntent().getAction(), Intent.ACTION_SEND) |
| || TextUtils.equals(getIntent().getAction(), Intent.ACTION_VIEW)) { |
| feedUrl = (TextUtils.equals(getIntent().getAction(), Intent.ACTION_SEND)) |
| ? getIntent().getStringExtra(Intent.EXTRA_TEXT) : getIntent().getDataString(); |
| getSupportActionBar().setTitle(R.string.add_new_feed_label); |
| } else { |
| throw new IllegalArgumentException("Activity must be started with feedurl argument!"); |
| } |
| |
| Log.d(TAG, "Activity was started with url " + feedUrl); |
| setLoadingLayout(); |
| if (savedInstanceState == null) { |
| startFeedDownload(feedUrl, null, null); |
| } else { |
| startFeedDownload(feedUrl, savedInstanceState.getString("username"), savedInstanceState.getString("password")); |
| } |
| } |
| |
| /** |
| * Displays a progress indicator. |
| */ |
| private void setLoadingLayout() { |
| RelativeLayout rl = new RelativeLayout(this); |
| RelativeLayout.LayoutParams rlLayoutParams = new RelativeLayout.LayoutParams( |
| LinearLayout.LayoutParams.MATCH_PARENT, |
| LinearLayout.LayoutParams.MATCH_PARENT); |
| |
| ProgressBar pb = new ProgressBar(this); |
| pb.setIndeterminate(true); |
| RelativeLayout.LayoutParams pbLayoutParams = new RelativeLayout.LayoutParams( |
| LinearLayout.LayoutParams.WRAP_CONTENT, |
| LinearLayout.LayoutParams.WRAP_CONTENT); |
| pbLayoutParams.addRule(RelativeLayout.CENTER_IN_PARENT); |
| rl.addView(pb, pbLayoutParams); |
| addContentView(rl, rlLayoutParams); |
| } |
| |
| @Override |
| protected void onResume() { |
| super.onResume(); |
| isPaused = false; |
| EventDistributor.getInstance().register(listener); |
| EventBus.getDefault().register(this); |
| } |
| |
| @Override |
| protected void onPause() { |
| super.onPause(); |
| isPaused = true; |
| EventDistributor.getInstance().unregister(listener); |
| EventBus.getDefault().unregister(this); |
| } |
| |
| @Override |
| protected void onStop() { |
| super.onStop(); |
| if (downloader != null && !downloader.isFinished()) { |
| downloader.cancel(); |
| } |
| if(dialog != null && dialog.isShowing()) { |
| dialog.dismiss(); |
| } |
| } |
| |
| @Override |
| public void onDestroy() { |
| super.onDestroy(); |
| if(updater != null) { |
| updater.unsubscribe(); |
| } |
| if(download != null) { |
| download.unsubscribe(); |
| } |
| if(parser != null) { |
| parser.unsubscribe(); |
| } |
| } |
| |
| @Override |
| protected void onSaveInstanceState(Bundle outState) { |
| super.onSaveInstanceState(outState); |
| if (feed != null && feed.getPreferences() != null) { |
| outState.putString("username", feed.getPreferences().getUsername()); |
| outState.putString("password", feed.getPreferences().getPassword()); |
| } |
| } |
| |
| private void resetIntent(String url, String title) { |
| Intent intent = new Intent(); |
| intent.putExtra(ARG_FEEDURL, url); |
| intent.putExtra(ARG_TITLE, title); |
| setIntent(intent); |
| } |
| |
| @Override |
| public boolean onOptionsItemSelected(MenuItem item) { |
| switch (item.getItemId()) { |
| case android.R.id.home: |
| Intent destIntent = new Intent(this, MainActivity.class); |
| if (NavUtils.shouldUpRecreateTask(this, destIntent)) { |
| startActivity(destIntent); |
| } else { |
| NavUtils.navigateUpFromSameTask(this); |
| } |
| return true; |
| } |
| return super.onOptionsItemSelected(item); |
| } |
| |
| private void startFeedDownload(String url, String username, String password) { |
| Log.d(TAG, "Starting feed download"); |
| url = URLChecker.prepareURL(url); |
| feed = new Feed(url, null); |
| if (username != null && password != null) { |
| feed.setPreferences(new FeedPreferences(0, false, FeedPreferences.AutoDeleteAction.GLOBAL, username, password)); |
| } |
| String fileUrl = new File(getExternalCacheDir(), |
| FileNameGenerator.generateFileName(feed.getDownload_url())).toString(); |
| feed.setFile_url(fileUrl); |
| final DownloadRequest request = new DownloadRequest(feed.getFile_url(), |
| feed.getDownload_url(), "OnlineFeed", 0, Feed.FEEDFILETYPE_FEED, username, password, |
| true, null); |
| |
| download = Observable.create(new Observable.OnSubscribe<DownloadStatus>() { |
| @Override |
| public void call(Subscriber<? super DownloadStatus> subscriber) { |
| feeds = DBReader.getFeedList(); |
| downloader = new HttpDownloader(request); |
| downloader.call(); |
| Log.d(TAG, "Download was completed"); |
| subscriber.onNext(downloader.getResult()); |
| subscriber.onCompleted(); |
| } |
| }) |
| .subscribeOn(Schedulers.newThread()) |
| .observeOn(AndroidSchedulers.mainThread()) |
| .subscribe(status -> { |
| if (status != null) { |
| if (!status.isCancelled()) { |
| if (status.isSuccessful()) { |
| parseFeed(); |
| } else if (status.getReason() == DownloadError.ERROR_UNAUTHORIZED) { |
| if (!isFinishing() && !isPaused) { |
| dialog = new FeedViewAuthenticationDialog(OnlineFeedViewActivity.this, |
| R.string.authentication_notification_title, downloader.getDownloadRequest().getSource()); |
| dialog.show(); |
| } |
| } else { |
| String errorMsg = status.getReason().getErrorString(OnlineFeedViewActivity.this); |
| if (errorMsg != null && status.getReasonDetailed() != null) { |
| errorMsg += " (" + status.getReasonDetailed() + ")"; |
| } |
| showErrorDialog(errorMsg); |
| } |
| } |
| } else { |
| Log.wtf(TAG, "DownloadStatus returned by Downloader was null"); |
| finish(); |
| } |
| }); |
| } |
| |
| private void parseFeed() { |
| if (feed == null || feed.getFile_url() == null && feed.isDownloaded()) { |
| throw new IllegalStateException("feed must be non-null and downloaded when parseFeed is called"); |
| } |
| Log.d(TAG, "Parsing feed"); |
| |
| parser = Observable.create(new Observable.OnSubscribe<FeedHandlerResult>() { |
| @Override |
| public void call(Subscriber<? super FeedHandlerResult> subscriber) { |
| FeedHandler handler = new FeedHandler(); |
| try { |
| FeedHandlerResult result = handler.parseFeed(feed); |
| subscriber.onNext(result); |
| } catch (UnsupportedFeedtypeException e) { |
| Log.d(TAG, "Unsupported feed type detected"); |
| if (TextUtils.equals("html", e.getRootElement().toLowerCase())) { |
| showFeedDiscoveryDialog(new File(feed.getFile_url()), feed.getDownload_url()); |
| } else { |
| subscriber.onError(e); |
| } |
| } catch (Exception e) { |
| subscriber.onError(e); |
| } finally { |
| boolean rc = new File(feed.getFile_url()).delete(); |
| Log.d(TAG, "Deleted feed source file. Result: " + rc); |
| subscriber.onCompleted(); |
| } |
| } |
| }) |
| .subscribeOn(Schedulers.newThread()) |
| .observeOn(AndroidSchedulers.mainThread()) |
| .subscribe(result -> { |
| beforeShowFeedInformation(result.feed); |
| showFeedInformation(result.feed, result.alternateFeedUrls); |
| }, error -> { |
| String errorMsg = DownloadError.ERROR_PARSER_EXCEPTION.getErrorString( |
| OnlineFeedViewActivity.this) + " (" + error.getMessage() + ")"; |
| showErrorDialog(errorMsg); |
| }); |
| } |
| |
| /** |
| * Called after the feed has been downloaded and parsed and before showFeedInformation is called. |
| * This method is executed on a background thread |
| */ |
| private void beforeShowFeedInformation(Feed feed) { |
| // remove HTML tags from descriptions |
| Log.d(TAG, "Removing HTML from shownotes"); |
| if (feed.getItems() != null) { |
| HtmlToPlainText formatter = new HtmlToPlainText(); |
| for (FeedItem item : feed.getItems()) { |
| if (item.getDescription() != null) { |
| Document description = Jsoup.parse(item.getDescription()); |
| item.setDescription(StringUtils.trim(formatter.getPlainText(description))); |
| } |
| } |
| } |
| } |
| |
| /** |
| * Called when feed parsed successfully. |
| * This method is executed on the GUI thread. |
| */ |
| private void showFeedInformation(final Feed feed, Map<String, String> alternateFeedUrls) { |
| setContentView(R.layout.listview_activity); |
| |
| this.feed = feed; |
| this.selectedDownloadUrl = feed.getDownload_url(); |
| EventDistributor.getInstance().register(listener); |
| ListView listView = (ListView) findViewById(R.id.listview); |
| LayoutInflater inflater = (LayoutInflater) |
| getSystemService(Context.LAYOUT_INFLATER_SERVICE); |
| View header = inflater.inflate(R.layout.onlinefeedview_header, listView, false); |
| listView.addHeaderView(header); |
| |
| listView.setAdapter(new FeedItemlistDescriptionAdapter(this, 0, feed.getItems())); |
| |
| ImageView cover = (ImageView) header.findViewById(R.id.imgvCover); |
| TextView title = (TextView) header.findViewById(R.id.txtvTitle); |
| TextView author = (TextView) header.findViewById(R.id.txtvAuthor); |
| TextView description = (TextView) header.findViewById(R.id.txtvDescription); |
| Spinner spAlternateUrls = (Spinner) header.findViewById(R.id.spinnerAlternateUrls); |
| |
| subscribeButton = (Button) header.findViewById(R.id.butSubscribe); |
| |
| if (feed.getImage() != null && StringUtils.isNotBlank(feed.getImage().getDownload_url())) { |
| Glide.with(this) |
| .load(feed.getImage().getDownload_url()) |
| .placeholder(R.color.light_gray) |
| .error(R.color.light_gray) |
| .diskCacheStrategy(ApGlideSettings.AP_DISK_CACHE_STRATEGY) |
| .fitCenter() |
| .dontAnimate() |
| .into(cover); |
| } |
| |
| title.setText(feed.getTitle()); |
| author.setText(feed.getAuthor()); |
| description.setText(feed.getDescription()); |
| |
| subscribeButton.setOnClickListener(v -> { |
| try { |
| Feed f = new Feed(selectedDownloadUrl, null, feed.getTitle()); |
| f.setPreferences(feed.getPreferences()); |
| this.feed = f; |
| |
| DownloadRequester.getInstance().downloadFeed(this, f); |
| } catch (DownloadRequestException e) { |
| e.printStackTrace(); |
| DownloadRequestErrorDialogCreator.newRequestErrorDialog(OnlineFeedViewActivity.this, |
| e.getMessage()); |
| } |
| setSubscribeButtonState(feed); |
| }); |
| |
| if (alternateFeedUrls.isEmpty()) { |
| spAlternateUrls.setVisibility(View.GONE); |
| } else { |
| spAlternateUrls.setVisibility(View.VISIBLE); |
| |
| final List<String> alternateUrlsList = new ArrayList<>(); |
| final List<String> alternateUrlsTitleList = new ArrayList<>(); |
| |
| alternateUrlsList.add(feed.getDownload_url()); |
| alternateUrlsTitleList.add(feed.getTitle()); |
| |
| |
| alternateUrlsList.addAll(alternateFeedUrls.keySet()); |
| for (String url : alternateFeedUrls.keySet()) { |
| alternateUrlsTitleList.add(alternateFeedUrls.get(url)); |
| } |
| ArrayAdapter<String> adapter = new ArrayAdapter<>(this, android.R.layout.simple_spinner_item, alternateUrlsTitleList); |
| adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); |
| spAlternateUrls.setAdapter(adapter); |
| spAlternateUrls.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { |
| @Override |
| public void onItemSelected(AdapterView<?> parent, View view, int position, long id) { |
| selectedDownloadUrl = alternateUrlsList.get(position); |
| } |
| |
| @Override |
| public void onNothingSelected(AdapterView<?> parent) { |
| |
| } |
| }); |
| } |
| setSubscribeButtonState(feed); |
| } |
| |
| private void setSubscribeButtonState(Feed feed) { |
| if (subscribeButton != null && feed != null) { |
| if (DownloadRequester.getInstance().isDownloadingFile(feed.getDownload_url())) { |
| subscribeButton.setEnabled(false); |
| subscribeButton.setText(R.string.downloading_label); |
| } else if (feedInFeedlist(feed)) { |
| subscribeButton.setEnabled(false); |
| subscribeButton.setText(R.string.subscribed_label); |
| } else { |
| subscribeButton.setEnabled(true); |
| subscribeButton.setText(R.string.subscribe_label); |
| } |
| } |
| } |
| |
| private boolean feedInFeedlist(Feed feed) { |
| if (feeds == null || feed == null) { |
| return false; |
| } |
| for (Feed f : feeds) { |
| if (f.getIdentifyingValue().equals(feed.getIdentifyingValue())) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| private void showErrorDialog(String errorMsg) { |
| assert(Looper.myLooper() == Looper.getMainLooper()); // run on UI thread |
| if (!isFinishing() && !isPaused) { |
| AlertDialog.Builder builder = new AlertDialog.Builder(this); |
| builder.setTitle(R.string.error_label); |
| if (errorMsg != null) { |
| builder.setMessage(getString(R.string.error_msg_prefix) + errorMsg); |
| } else { |
| builder.setMessage(R.string.error_msg_prefix); |
| } |
| builder.setNeutralButton(android.R.string.ok, |
| (dialog, which) -> { |
| dialog.cancel(); |
| } |
| ); |
| builder.setOnCancelListener(dialog -> { |
| setResult(RESULT_ERROR); |
| finish(); |
| }); |
| if(dialog != null && dialog.isShowing()) { |
| dialog.dismiss(); |
| } |
| dialog = builder.show(); |
| } |
| } |
| |
| private void showFeedDiscoveryDialog(File feedFile, String baseUrl) { |
| FeedDiscoverer fd = new FeedDiscoverer(); |
| final Map<String, String> urlsMap; |
| try { |
| urlsMap = fd.findLinks(feedFile, baseUrl); |
| if (urlsMap == null || urlsMap.isEmpty()) { |
| return; |
| } |
| } catch (IOException e) { |
| e.printStackTrace(); |
| return; |
| } |
| |
| if (isPaused || isFinishing()) { |
| return; |
| } |
| |
| final List<String> titles = new ArrayList<>(); |
| final List<String> urls = new ArrayList<>(); |
| |
| urls.addAll(urlsMap.keySet()); |
| for (String url : urls) { |
| titles.add(urlsMap.get(url)); |
| } |
| |
| final ArrayAdapter<String> adapter = new ArrayAdapter<>(OnlineFeedViewActivity.this, R.layout.ellipsize_start_listitem, R.id.txtvTitle, titles); |
| DialogInterface.OnClickListener onClickListener = (dialog, which) -> { |
| String selectedUrl = urls.get(which); |
| dialog.dismiss(); |
| resetIntent(selectedUrl, titles.get(which)); |
| FeedPreferences prefs = feed.getPreferences(); |
| if(prefs != null) { |
| startFeedDownload(selectedUrl, prefs.getUsername(), prefs.getPassword()); |
| } else { |
| startFeedDownload(selectedUrl, null, null); |
| } |
| }; |
| |
| AlertDialog.Builder ab = new AlertDialog.Builder(OnlineFeedViewActivity.this) |
| .setTitle(R.string.feeds_label) |
| .setCancelable(true) |
| .setOnCancelListener(dialog -> finish()) |
| .setAdapter(adapter, onClickListener); |
| |
| runOnUiThread(() -> { |
| if(dialog != null && dialog.isShowing()) { |
| dialog.dismiss(); |
| } |
| dialog = ab.show(); |
| }); |
| } |
| |
| private class FeedViewAuthenticationDialog extends AuthenticationDialog { |
| |
| private String feedUrl; |
| |
| public FeedViewAuthenticationDialog(Context context, int titleRes, String feedUrl) { |
| super(context, titleRes, true, false, null, null); |
| this.feedUrl = feedUrl; |
| } |
| |
| @Override |
| protected void onCancelled() { |
| super.onCancelled(); |
| finish(); |
| } |
| |
| @Override |
| protected void onConfirmed(String username, String password, boolean saveUsernamePassword) { |
| startFeedDownload(feedUrl, username, password); |
| } |
| } |
| |
| } |