/*
* Copyright (c) 2014-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.facebook.stetho.sample;
import android.content.ContentProviderOperation;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.OperationApplicationException;
import android.net.Uri;
import android.os.RemoteException;
import android.util.Log;
import android.util.Xml;
import com.facebook.stetho.common.Utf8Charset;
import com.facebook.stetho.common.Util;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import javax.annotation.Nullable;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
public class APODRssFetcher {
private static final String TAG = "APODRssFetcher";
private static final String APOD_RSS_URL = "https://apod.nasa.gov/apod.rss";
private final ContentResolver mContentResolver;
public APODRssFetcher(ContentResolver contentResolver) {
mContentResolver = contentResolver;
}
public void fetchAndStore() {
Networker.HttpRequest request = Networker.HttpRequest.newBuilder()
.friendlyName("APOD RSS")
.method(Networker.HttpMethod.GET)
.url(APOD_RSS_URL)
.build();
Networker.get().submit(request, mStoreRssResponse);
}
private final Networker.Callback mStoreRssResponse = new Networker.Callback() {
@Override
public void onResponse(Networker.HttpResponse result) {
if (result.statusCode == 200) {
try {
List<RssItem> rssItems = parseRss(result.body);
List<ApodItem> apodItems = decorateRssItemsWithLinkImages(rssItems);
store(apodItems);
} catch (XmlPullParserException e) {
Log.e(TAG, "Parse error", e);
} catch (OperationApplicationException e) {
Log.e(TAG, "Database write error", e);
} catch (RemoteException e) {
// Not recoverable, our process or the system_server must be dying...
throw new RuntimeException(e);
} catch (IOException e) {
// Reading from a byte[] shouldn't cause this...
throw new RuntimeException(e);
}
}
}
@Override
public void onFailure(IOException e) {
// Show in Stetho :)
}
private List<RssItem> parseRss(byte[] body) throws IOException, XmlPullParserException {
XmlPullParser parser = Xml.newPullParser();
parser.setInput(new ByteArrayInputStream(body), "UTF-8");
List<RssItem> items = new RssParser(parser).parse();
Log.d(TAG, "Fetched " + items.size() + " items");
return items;
}
public List<ApodItem> decorateRssItemsWithLinkImages(List<RssItem> rssItems) {
ArrayList<ApodItem> apodItems = new ArrayList<>(rssItems.size());
final CountDownLatch fetchLinkLatch = new CountDownLatch(rssItems.size());
for (RssItem rssItem : rssItems) {
final ApodItem apodItem = new ApodItem();
apodItem.rssItem = rssItem;
fetchLinkPage(rssItem.link, new PageScrapedCallback() {
@Override
public void onPageScraped(@Nullable List<String> imageUrls) {
apodItem.largeImageUrl = imageUrls != null && !imageUrls.isEmpty()
? imageUrls.get(0)
: null;
fetchLinkLatch.countDown();
}
});
apodItems.add(apodItem);
}
// Wait for all link fetches to complete, despite running them in parallel...
Util.awaitUninterruptibly(fetchLinkLatch);
return apodItems;
}
private void store(List<ApodItem> items) throws RemoteException, OperationApplicationException {
ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>();
operations.add(
ContentProviderOperation.newDelete(APODContract.CONTENT_URI)
.build());
for (ApodItem item : items) {
Log.d(TAG, "Add item: " + item.rssItem.title);
operations.add(
ContentProviderOperation.newInsert(APODContract.CONTENT_URI)
.withValues(convertItemToValues(item))
.build());
}
mContentResolver.applyBatch(APODContract.AUTHORITY, operations);
}
private ContentValues convertItemToValues(ApodItem item) {
ContentValues values = new ContentValues();
values.put(APODContract.Columns.TITLE, item.rssItem.title);
ArrayList<String> imageUrls = new ArrayList<>();
String strippedText = HtmlScraper.parseWithImageTags(
item.rssItem.description,
null /* origin */,
imageUrls);
// Hack to remove some strange non-printing character at the start...
strippedText = strippedText.substring(1).trim();
String imageUrl = !imageUrls.isEmpty() ? imageUrls.get(0) : null;
values.put(APODContract.Columns.DESCRIPTION_IMAGE_URL, imageUrl);
values.put(APODContract.Columns.DESCRIPTION_TEXT, strippedText);
values.put(APODContract.Columns.LARGE_IMAGE_URL, item.largeImageUrl);
return values;
}
};
private void fetchLinkPage(String linkUrl, PageScrapedCallback callback) {
String originUrl = getOriginUri(Uri.parse(linkUrl)).toString();
Networker.HttpRequest request = Networker.HttpRequest.newBuilder()
.friendlyName("fetchLinkPage")
.method(Networker.HttpMethod.GET)
.url(linkUrl)
.build();
Networker.get().submit(request, new PageScrapeNetworkCallback(originUrl, callback));
}
private static Uri getOriginUri(Uri uri) {
Uri.Builder b = uri.buildUpon();
b.encodedPath(null);
List<String> segments = uri.getPathSegments();
for (int i = 0; i < segments.size() - 1; i++) {
b.appendEncodedPath(segments.get(i));
}
return b.build();
}
private static class PageScrapeNetworkCallback implements Networker.Callback {
@Nullable private final String mOrigin;
private final PageScrapedCallback mDelegate;
public PageScrapeNetworkCallback(@Nullable String origin, PageScrapedCallback delegate) {
mOrigin = origin;
mDelegate = delegate;
}
@Override
public void onResponse(Networker.HttpResponse result) {
ArrayList<String> imageUrls = new ArrayList<>();
String htmlText = Utf8Charset.decodeUTF8(result.body);
HtmlScraper.parseWithImageTags(htmlText, mOrigin, imageUrls);
mDelegate.onPageScraped(imageUrls);
}
@Override
public void onFailure(IOException e) {
mDelegate.onPageScraped(null /* imageUrls */);
}
}
private static class RssParser {
private final XmlPullParser mParser;
public RssParser(XmlPullParser parser) {
mParser = parser;
}
public List<RssItem> parse() throws IOException, XmlPullParserException {
ArrayList<RssItem> items = new ArrayList<RssItem>();
mParser.nextTag();
mParser.require(XmlPullParser.START_TAG, null, "rss");
mParser.nextTag();
mParser.require(XmlPullParser.START_TAG, null, "channel");
while (mParser.next() != XmlPullParser.END_TAG) {
if (mParser.getEventType() != XmlPullParser.START_TAG) {
continue;
}
String name = mParser.getName();
if (name.equals("item")) {
items.add(readItem());
} else {
skip();
}
}
return items;
}
private RssItem readItem() throws XmlPullParserException, IOException {
mParser.require(XmlPullParser.START_TAG, null, "item");
RssItem item = new RssItem();
while (mParser.next() != XmlPullParser.END_TAG) {
if (mParser.getEventType() != XmlPullParser.START_TAG) {
continue;
}
String name = mParser.getName();
if (name.equals("title")) {
item.title = readTextFromTag("title");
} else if (name.equals("description")) {
item.description = readTextFromTag("description");
} else if (name.equals("link")) {
item.link = readTextFromTag("link");
} else {
skip();
}
}
return item;
}
private String readTextFromTag(String tagName) throws IOException, XmlPullParserException {
mParser.require(XmlPullParser.START_TAG, null, tagName);
String text = readText();
mParser.require(XmlPullParser.END_TAG, null, tagName);
return text;
}
private String readText() throws IOException, XmlPullParserException {
String result = "";
if (mParser.next() == XmlPullParser.TEXT) {
result = mParser.getText();
mParser.nextTag();
}
return result;
}
private void skip() throws IOException, XmlPullParserException {
if (mParser.getEventType() != XmlPullParser.START_TAG) {
throw new IllegalStateException();
}
int depth = 1;
while (depth != 0) {
switch (mParser.next()) {
case XmlPullParser.END_TAG:
depth--;
break;
case XmlPullParser.START_TAG:
depth++;
break;
}
}
}
}
private static class RssItem {
public String title;
public String description;
public String link;
}
private static class ApodItem {
public RssItem rssItem;
@Nullable public String largeImageUrl;
}
private interface PageScrapedCallback {
/**
* @param imageUrls Image URLs that were scraped or null if the page could not be fetched or
* parsed.
*/
public void onPageScraped(@Nullable List<String> imageUrls);
}
}