// Copyright 2009 Google Inc. // Copyright 2011 NPR // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package org.npr.android.news; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.pm.PackageManager; import android.graphics.drawable.Drawable; import android.os.Bundle; import android.os.Environment; import android.text.Html; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.webkit.WebView; import android.widget.Button; import android.widget.FrameLayout; import android.widget.TextView; import org.npr.android.util.DisplayUtils; import org.npr.android.util.PlaylistEntry; import org.npr.android.util.PlaylistRepository; import org.npr.android.widget.WorkspaceView; import org.npr.api.Book; import org.npr.api.Story; import org.npr.api.Story.Byline; import org.npr.api.Story.TextWithHtml; import java.text.DateFormat; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.Map; public class NewsStoryActivity extends RootActivity implements WorkspaceView.OnScreenChangeListener { private static final String LOG_TAG = NewsStoryActivity.class.getName(); private WorkspaceView workspace; private LayoutInflater inflater; private ImageThreadLoader imageLoader; // Sample date from api: Tue, 09 Jun 2009 15:20:00 -0400 public static final SimpleDateFormat apiDateFormat = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss Z"); private final DateFormat longDateFormat = DateFormat.getDateInstance(DateFormat.LONG); private static class TrackerItem { String title; String topicId; String orgId; String storyId; } private TrackerItem trackerItem = null; private List<Story> stories; private boolean externalStorageAvailable = false; private PlaylistRepository playlistRepository; private BroadcastReceiver playlistChangedReceiver; private BroadcastReceiver playbackChangedReceiver; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); stories = new ArrayList<Story>(); playlistRepository = new PlaylistRepository(getApplicationContext(), getContentResolver()); final String storyIdsString = getIntent().getStringExtra(Constants.EXTRA_STORY_ID_LIST); Log.d(LOG_TAG, "Got the following id's: " + storyIdsString); String currentStoryId = ""; if (getIntent().hasExtra(Constants.EXTRA_STORY_ID)) { currentStoryId = getIntent().getStringExtra(Constants.EXTRA_STORY_ID); } else if (getIntent().hasExtra(Constants.EXTRA_ACTIVITY_DATA)) { currentStoryId = getIntent().getStringExtra(Constants.EXTRA_ACTIVITY_DATA); } String[] storyIds; if (storyIdsString == null) { storyIds = new String[]{currentStoryId}; } else { storyIds = storyIdsString.split(","); } if (storyIds.length == 0) { finish(); } String state = Environment.getExternalStorageState(); externalStorageAvailable = Environment.MEDIA_MOUNTED.equals(state) || Environment.MEDIA_MOUNTED_READ_ONLY.equals(state); workspace = new WorkspaceView(this, null); inflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE); imageLoader = ImageThreadLoader.getOnDiskInstance(this); FrameLayout.LayoutParams layout = new FrameLayout.LayoutParams( FrameLayout.LayoutParams.FILL_PARENT, FrameLayout.LayoutParams.FILL_PARENT ); layout.setMargins(0, 0, 0, DisplayUtils.convertToDIP(this, 95)); ((ViewGroup) findViewById(R.id.TitleContent)).addView(workspace, layout); boolean teaserOnly = getIntent().getBooleanExtra(Constants.EXTRA_TEASER_ONLY, false); for (int i = 0; i < storyIds.length; i++) { String storyId = storyIds[i]; Story story = NewsListActivity.getStoryFromCache(storyId, this); stories.add(story); layoutStory(story, i, storyIds.length, teaserOnly); if (storyId.equals(currentStoryId)) { trackerItem = new TrackerItem(); workspace.setCurrentScreen(i); List<Story.Organization> organizations = story.getOrganizations(); if (organizations != null && organizations.size() > 0) { trackerItem.orgId = organizations.get(0).getId(); } for (Story.Parent p : story.getParents()) { if (p.isPrimary()) { trackerItem.topicId = p.getId(); break; } } trackerItem.title = story.getTitle(); trackerItem.storyId = story.getId(); } } playlistChangedReceiver = new PlaylistChangedReceiver(); this.registerReceiver(playlistChangedReceiver, new IntentFilter(PlaylistRepository.PLAYLIST_CHANGED)); playbackChangedReceiver = new PlaybackChangedReceiver(); Intent intent = this.registerReceiver(playbackChangedReceiver, new IntentFilter(PlaybackService.SERVICE_CHANGE_NAME)); if (intent != null) { playbackChangedReceiver.onReceive(this, intent); } workspace.setOnScreenChangeListener(this); } @Override protected void onStop() { if (playlistChangedReceiver != null) { unregisterReceiver(playlistChangedReceiver); playlistChangedReceiver = null; } if (playbackChangedReceiver != null) { unregisterReceiver(playbackChangedReceiver); playbackChangedReceiver = null; } super.onStop(); } private void layoutStory(Story story, int position, int total, boolean teaserOnly) { if (position >= stories.size()) { Log.e(LOG_TAG, "Attempt to get story view for position " + position + " beyond loaded stories"); return; } if (story == null) { Log.e(LOG_TAG, "Story at position " + position + " is null?"); return; } View storyView = inflater.inflate(R.layout.news_story, null, false); workspace.addView(storyView); loadStory(story, position, total, storyView, teaserOnly); Button listenNow = (Button) storyView.findViewById(R.id.NewsStoryListenNowButton); Button enqueue = (Button) storyView.findViewById(R.id.NewsStoryListenEnqueueButton); Button share = (Button) storyView.findViewById(R.id.NewsStoryShareButton); StoryClickListener listener = new StoryClickListener(position); listenNow.setOnClickListener(listener); enqueue.setOnClickListener(listener); share.setOnClickListener(listener); boolean isListenable = story.getPlayableUrl() != null; listenNow.setVisibility(isListenable ? View.VISIBLE : View.INVISIBLE); listenNow.setEnabled(isListenable); enqueue.setVisibility(isListenable ? View.VISIBLE : View.INVISIBLE); enqueue.setEnabled(isListenable && playlistRepository.getPlaylistItemFromStoryId(story.getId()) == null); } private void loadStory(Story story, int position, int total, View storyView, boolean teaserOnly) { WebView storyWebView = (WebView) storyView.findViewById(R.id.NewsStoryWebView); storyWebView.getSettings().setJavaScriptEnabled(true); storyWebView.setBackgroundColor(0); storyWebView.addJavascriptInterface(new ImageClickInterface(this), "click"); TextView index = (TextView) storyView.findViewById(R.id.NewsStoryIndex); TextView title = (TextView) storyView.findViewById(R.id.NewsStoryTitleText); TextView dateline = (TextView) storyView.findViewById(R.id.NewsStoryDateline); TextView byline = (TextView) storyView.findViewById(R.id.NewsStoryByline); index.setText(String.format(getString(R.string.msg_story_count_format), position + 1, total)); title.setText(Html.fromHtml(story.getTitle())); StringBuilder datelineText = new StringBuilder(); try { datelineText.append( longDateFormat.format( apiDateFormat.parse(story.getStoryDate()) ) ); } catch (ParseException e) { Log.e(LOG_TAG, "date format", e); } Iterator<Byline> bylines = story.getBylines().iterator(); if (bylines.hasNext()) { StringBuilder bylineText = new StringBuilder("by "); while (bylines.hasNext()) { Byline b = bylines.next(); bylineText.append(b.getName()); if (bylines.hasNext()) { bylineText.append(", "); } } byline.setText(bylineText.toString() .replaceFirst(", ([^,]+)$", " & $1")); } else { byline.setVisibility(View.GONE); } String durationString = story.getDuration(); if (durationString != null) { try { int duration = Integer.parseInt(durationString); if (duration > 0) { if (datelineText.length() > 0) { datelineText.append(" | "); } datelineText.append( String.format("%d min %02d sec", duration / 60, duration % 60) ); } } catch (NumberFormatException e) { Log.w(LOG_TAG, "Invalid duration: " + durationString, e); } } if (datelineText.length() == 0) { dateline.setVisibility(View.GONE); } else { dateline.setText(datelineText.toString()); } TextWithHtml text = story.getTextWithHtml(); String textHtml; if (!teaserOnly && text != null) { StringBuilder sb = new StringBuilder(); if (story.getLayout() != null && story.getLayout().getItems().size() > 0) { for (Map.Entry<Integer, Story.Layout.LayoutItem> entry : story.getLayout().getItems().entrySet()) { if (entry.getValue().getType() == Story.Layout.Type.text) { Integer paragraphNum = entry.getKey(); try { paragraphNum = Integer.parseInt(entry.getValue().getItemId()); } catch (NumberFormatException e) { Log.w(LOG_TAG, "Unable to parse paragraph number: " + entry.getValue().getItemId()); } String paragraph = text.getParagraphs().get(paragraphNum); // WebView can't load external images, so we need to strip them or it // may not render. paragraph = paragraph.replaceAll("<img .*/>", ""); sb.append("<p>").append(paragraph).append("</p>"); } else if (entry.getValue().getType() == Story.Layout.Type.image && externalStorageAvailable) { Story.Image image = story.getImages().get(entry.getValue().getItemId()); if (image != null) { imageLoader.loadImage(image.getSrc(), new ImageLoadListener(position)); String imageTag = String.format( "<a onClick=\"window.click.clickOnImage('%s', '%s', '%s')\">" + "<div id=\"story-icon\"><img src=\"file://%s/%s\" /></div></a>", image.getSrc(), image.getCaption().replace("'", "\\'").replace("\"", """), image.getAttribution().replace("'", "\\'").replace("\"", """), ImageThreadLoader.DiskCache.getCachePath(this), ImageThreadLoader.DiskCache.makeCacheFileName(image.getSrc()) ); Log.d(LOG_TAG, "Adding tag for image " + imageTag); sb.append(imageTag); } } } } else { // No layout? Just add paragraphs for (Map.Entry<Integer, String> entry : text.getParagraphs().entrySet()) { String paragraph = entry.getValue(); // WebView can't load external images, so we need to strip them or it // may not render. paragraph = paragraph.replaceAll("<img .*/>", ""); sb.append("<p>").append(paragraph).append("</p>"); } } textHtml = String.format(HTML_FORMAT, sb.toString()); // Load any book parents for (Story.Parent parent : story.getParents()) { if (parent.getType().equals("book")) { sb = new StringBuilder(textHtml); List<Book> books = NewsListActivity.getBooksFromCache(story.getId()); if (books != null) { for (Book book : books) { sb.append("<hr/>"); if (book.getPromoArt() != null) { sb.append( String.format( "<div id=\"story-icon\"><img src=\"%s\" /></div>", book.getPromoArt()) ); } sb.append(String.format("<p><b>%s</b><br/>", book.getTitle())); sb.append(String.format("<i>By %s</i></p>", book.getAuthor())); sb.append(book.getText()); } } textHtml = String.format(HTML_FORMAT, sb.toString()); // Book loads all books for the story, so break after one is found break; } } } else { // Only show the teaser if there is no full-text. textHtml = String.format(HTML_FORMAT, "<p class='teaser'>" + story.getTeaser() + "</p>"); } storyWebView.loadDataWithBaseURL(null, textHtml, "text/html", "utf-8", null); } private void playStory(boolean playNow, int position) { if (position >= stories.size() || position == -1) { Log.e(LOG_TAG, "Attempt to get story audio for position " + position + " beyond loaded stories"); return; } Story story = stories.get(position); if (story == null) { Log.e(LOG_TAG, "Story at position " + position + " is null?"); return; } String url = story.getPlayableUrl(); if (playNow) { PlaylistEntry activeEntry = playlistRepository.getPlaylistItemFromId(getActiveId()); long playlistId; if (activeEntry != null) { playlistId = playlistRepository.insert(story, activeEntry.playOrder + 1); } else { playlistId = playlistRepository.add(story); } PlaylistEntry entry = playlistRepository.getPlaylistItemFromId(playlistId); this.playEntryNow(entry); } else { playlistRepository.add(story); } } // WebView is default black text. // Also add formatting for the image, if there is one. private static final String HTML_FORMAT = "<!DOCTYPE html PUBLIC \"-//W3C//DTD HTML 4.01//EN\">" + "<html><head><title></title>" + "<style type=\"text/css\">" + "body {color:#000; margin:0; font-size:10pt;}" + "a {color:blue}" + ".teaser {font-size: 10pt}" + "#story-icon {width: 100px; float:left; " + "margin-right: 6pt; margin-bottom: 3pt;}" + "#story-icon img {vertical-align: middle; width: 100%%;}" + "</style>" + "</head>" + "<body>" + "%s" + "</body></html>"; @Override public void onScreenChanged(int newPosition) { } /** * Position-aware click listener for each story view so that when the play * buttons are clicked we know which story's button got clicked. */ private class StoryClickListener implements View.OnClickListener { private final int position; public StoryClickListener(int position) { this.position = position; } @Override public void onClick(View v) { Log.d(LOG_TAG, "Click registered for view " + position); switch (v.getId()) { case R.id.NewsStoryListenNowButton: playStory(true, position); break; case R.id.NewsStoryListenEnqueueButton: playStory(false, position); break; case R.id.NewsStoryShareButton: if (position >= stories.size()) { Log.e(LOG_TAG, "Attempt to get story audio for position " + position + " beyond loaded stories"); } else { Story story = stories.get(position); if (story == null) { Log.e(LOG_TAG, "Story at position " + position + " is null?"); } else { String shortLink = story.getShortLink(); if (shortLink == null) { shortLink = "http://npr.org/" + story.getId(); } Intent shareIntent = new Intent(android.content.Intent.ACTION_SEND); shareIntent.putExtra(Intent.EXTRA_SUBJECT, story.getTitle()); shareIntent.putExtra(Intent.EXTRA_TEXT, shortLink); shareIntent.setType("text/plain"); startActivity(Intent.createChooser(shareIntent, getString(R.string.msg_share_story))); } } break; } } } private class PlaylistChangedReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { int len = stories.size(); for (int i = 0; i < len; i++) { View v = workspace.getChildAt(i); Button enqueue = (Button) v.findViewById(R.id.NewsStoryListenEnqueueButton); enqueue.setEnabled( playlistRepository.getPlaylistItemFromStoryId(stories.get(i).getId()) == null); } } } private class PlaybackChangedReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { Long playlistId = -1L; Playable playable = null; try { Context serviceContext = context.createPackageContext(context.getPackageName(), Context.CONTEXT_INCLUDE_CODE | Context.CONTEXT_IGNORE_SECURITY); Bundle bundle = intent.getExtras(); bundle.setClassLoader(serviceContext.getClassLoader()); playable = bundle.getParcelable(Playable.PLAYABLE_TYPE); } catch (PackageManager.NameNotFoundException e) { Log.e(LOG_TAG, "Unable to parse playing item information", e); } if (playable != null) { playlistId = playable.getId(); } if (playlistId != -1) { PlaylistEntry pe = playlistRepository.getPlaylistItemFromId (playlistId); if (pe == null) return; int len = stories.size(); for (int i = 0; i < len; i++) { View v = workspace.getChildAt(i); Button listenNow = (Button) v.findViewById(R.id.NewsStoryListenNowButton); listenNow.setEnabled(!stories.get(i).getId().equals(pe.storyID)); } } } } private class ImageLoadListener implements ImageThreadLoader.ImageLoadedListener { int position; public ImageLoadListener(int position) { this.position = position; } public void imageLoaded(Drawable imageBitmap) { boolean teaserOnly = getIntent().getBooleanExtra(Constants.EXTRA_TEASER_ONLY, false); loadStory( stories.get(position), position, stories.size(), workspace.getChildAt(position), teaserOnly); } } final class ImageClickInterface { private Context context; public ImageClickInterface(Context context) { this.context = context; } @SuppressWarnings("unused") public void clickOnImage(final String url, final String caption, final String provider) { Log.v("Image click url: ", url + ", " + caption + ", " + provider); if (url != null && url.length() > 0) { Intent intent = new Intent(context, NewsImageActivity.class); intent.putExtra(NewsImageActivity.EXTRA_IMAGE_URL, url); intent.putExtra(NewsImageActivity.EXTRA_IMAGE_CAPTION, caption); intent.putExtra(NewsImageActivity.EXTRA_IMAGE_PROVIDER, provider); startActivityWithoutAnimation(intent); } } } }