// 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.os.Bundle; import android.os.Handler; import android.os.Message; import android.util.Log; import android.view.GestureDetector; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.view.animation.Animation; import android.view.animation.AnimationUtils; import android.widget.*; import android.widget.AdapterView.OnItemClickListener; import org.npr.android.util.*; import org.npr.api.ApiConstants; import org.npr.api.Book; import org.npr.api.Story; import info.guardianproject.onionkit.ui.OrbotHelper; import java.util.HashMap; import java.util.List; import java.util.Map; public class NewsListActivity extends TitleActivity implements OnItemClickListener { private static final String LOG_TAG = NewsListActivity.class.getName(); private String description; private String grouping; private String topicId; private int initialSize; protected NewsListAdapter listAdapter; private ListView listView; private static final Map<String, Story> storyCache = new HashMap<String, Story>(); private static final Map<String, List<Book>> bookCache = new HashMap<String, List<Book>>(); private GestureDetector gestureDetector; private Story flungStory; // Need to store this from the long-press event to ignore the click event private int lastLongPressPosition = -1; private BroadcastReceiver playlistChangedReceiver; // Message handler to communicate between the gestures and the activity private final Handler handler = new Handler() { @Override public void handleMessage(Message msg) { // Ignore when list is empty, e.g. no connectivity if (listAdapter.getCount() == 0) { return; } switch (msg.what) { case ListItemGestureListener.MSG_LONG_PRESS: { lastLongPressPosition = msg.arg1; Story longPressStory = listAdapter.getItem(msg.arg1); if (longPressStory != null && longPressStory.getPlayable() != null) { PlaylistRepository playlistRepository = new PlaylistRepository(getApplicationContext(), getContentResolver()); PlaylistEntry playlistEntry = playlistRepository.getPlaylistItemFromStoryId( longPressStory.getId()); if (playlistEntry == null) { addAndPulseIcon(listView.getChildAt(msg.arg1 - listView.getFirstVisiblePosition())); addStory(msg.arg1, true); } else { PlaylistEntry activeEntry = playlistRepository.getPlaylistItemFromId(getActiveId()); if (activeEntry != null) { playlistRepository.move(playlistEntry.playOrder, activeEntry.playOrder + 1); } playEntryNow(playlistEntry); } } } break; case ListItemGestureListener.MSG_FLING: { flungStory = listAdapter.getItem(msg.arg1); if (flungStory != null && flungStory.getPlayable() != null) { PlaylistRepository playlistRepository = new PlaylistRepository(getApplicationContext(), getContentResolver()); if (playlistRepository.getPlaylistItemFromStoryId( flungStory.getId() ) == null) { animateListItemFling( listView.getChildAt(msg.arg1 - listView.getFirstVisiblePosition()), msg.arg2, true ); addStory(msg.arg1, false); } else { animateListItemFling( listView.getChildAt(msg.arg1 - listView.getFirstVisiblePosition()), msg.arg2, false ); } } } break; } } }; public static Story getStoryFromCache(String storyId, Context context) { Story result = storyCache.get(storyId); if (result == null) { new StoryFetcher(storyId, storyCache, context).fetch(); } return result; } public static void addAllToStoryCache(List<Story> stories) { for (Story story : stories) { storyCache.put(story.getId(), story); } } public static List<Book> getBooksFromCache(String storyId) { return bookCache.get(storyId); } public static void addBooksToCache(String storyId, List<Book> books) { bookCache.put(storyId, books); } @Override public void onLowMemory() { super.onLowMemory(); storyCache.clear(); } @Override protected void onCreate(Bundle savedInstanceState) { OrbotHelper oc = new OrbotHelper(this); if (!oc.isOrbotInstalled()) { oc.promptToInstall(this); } else if (!oc.isOrbotRunning()) { oc.requestOrbotStart(this); } if (getIntent() == null || !(getIntent().hasExtra(Constants.EXTRA_QUERY_URL) || getIntent().hasExtra(Constants.EXTRA_PODCAST_URL))) { setDefaultIntent(); } grouping = getIntent().getStringExtra(Constants.EXTRA_GROUPING); topicId = getIntent().getStringExtra(Constants.EXTRA_TOPIC_ID); initialSize = getIntent().getIntExtra(Constants.EXTRA_SIZE, 0); description = getIntent().getStringExtra(Constants.EXTRA_DESCRIPTION); super.onCreate(savedInstanceState); // TODO: move this to a layout? View titleBar = findViewById(R.id.TitleBar); titleBar.setBackgroundDrawable(getResources().getDrawable( R.drawable.top_stories_title_background)); TextView titleText = (TextView) findViewById(R.id.TitleText); titleText.setTextColor(getResources().getColor(R.color.news_title_text)); TextView titleRight = (TextView) findViewById(R.id.TitleRight); titleRight.setTextColor(getResources().getColor(R.color.news_title_text)); /* ViewGroup bannerHolder = (ViewGroup) findViewById(R.id.SponsorshipBanner); ViewGroup.inflate(this, R.layout.banner, bannerHolder); bannerView = (BannerView) bannerHolder.getChildAt(0); bannerView.setPlayerView(getPlaylistView());*/ ViewGroup container = (ViewGroup) findViewById(R.id.Content); ViewGroup.inflate(this, R.layout.news, container); listView = (ListView) findViewById(R.id.ListView01); listView.setOnItemClickListener(this); listAdapter = new NewsListAdapter(this); listAdapter.setStoriesLoadedListener(listener); listView.setAdapter(listAdapter); // Gesture detection gestureDetector = new GestureDetector(getApplicationContext(), new ListItemGestureListener(listView, handler) ); View.OnTouchListener gestureListener = new View.OnTouchListener() { public boolean onTouch(View v, MotionEvent event) { return gestureDetector.onTouchEvent(event); } }; listView.setOnTouchListener(gestureListener); playlistChangedReceiver = new PlaylistChangedReceiver(); registerReceiver(playlistChangedReceiver, new IntentFilter(PlaylistRepository.PLAYLIST_CHANGED)); startIndeterminateProgressIndicator(); addStories(); } protected void startStoryLoadProgressIndicator() { FrameLayout storyLoadProgress = (FrameLayout) findViewById(R.id.StoryLoadBar); if (storyLoadProgress != null) { storyLoadProgress.setVisibility(View.VISIBLE); } } protected void stopStoryLoadProgressIndicator() { FrameLayout storyLoadProgress = (FrameLayout) findViewById(R.id.StoryLoadBar); if (storyLoadProgress != null) { storyLoadProgress.setVisibility(View.INVISIBLE); } } private NewsListAdapter.StoriesLoadedListener listener = new NewsListAdapter.StoriesLoadedListener() { @Override public void storiesLoaded() { stopIndeterminateProgressIndicator(); stopStoryLoadProgressIndicator(); } }; @Override protected void onStart() { super.onStart(); handler.postDelayed(updateTime, UPDATE_SHORT_PERIOD); } @Override protected void onStop() { if (playlistChangedReceiver != null) { unregisterReceiver(playlistChangedReceiver); playlistChangedReceiver = null; } handler.removeCallbacks(updateTime); super.onStop(); } @Override public void onItemClick(AdapterView<?> parent, View view, int position, long id) { // Ignore the click action after a long press on an audio track if (position == lastLongPressPosition) { lastLongPressPosition = -1; return; } Story s = (Story) parent.getAdapter().getItem(position); if (s == null) { startStoryLoadProgressIndicator(); addStories(); } else { Intent i = new Intent(this, NewsStoryActivity.class); i.putExtra( Constants.EXTRA_STORY_ID_LIST, listAdapter.getStoryIdList() ); i.putExtra(Constants.EXTRA_STORY_ID, s.getId()); if (getIntent().hasExtra(Constants.EXTRA_TEASER_ONLY)) { i.putExtra(Constants.EXTRA_TEASER_ONLY, getIntent().getBooleanExtra(Constants.EXTRA_TEASER_ONLY, false)); } startActivityWithoutAnimation(i); } } private void addStories() { String url = getApiUrl(); if (url != null) { // Adding these parameters to podcast urls (like WNYC) can break them Map<String, String> params = new HashMap<String, String>(); params.put("startNum", "" + listAdapter.getCount()); params.put("numResults", "" + initialSize); url = ApiConstants.instance().addParams(url, params); } else { url = getPodcastUrl(); } listAdapter.addMoreStories(url, initialSize); } private void addStory(int position, boolean playNow) { Story story = listAdapter.getItem(position); String url = story.getPlayableUrl(); if (url == null) { Log.w(LOG_TAG, "No audio for story " + position + " '" + listAdapter.getItem(position) + "'"); return; } PlaylistRepository playlistRepository = new PlaylistRepository(getApplicationContext(), getContentResolver()); 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); } } private void addAndPulseIcon(final View listItem) { final ImageView icon = (ImageView) listItem.findViewById(R.id .NewsItemIcon); if (icon == null) { Log.w(LOG_TAG, "Could not find the icon for list item: " + listItem); return; } Animation pulse = AnimationUtils.loadAnimation( NewsListActivity.this, R.anim.pulse_and_flatten ); pulse.setAnimationListener(new Animation.AnimationListener() { @Override public void onAnimationStart(Animation animation) { icon.setImageDrawable(getResources().getDrawable(R.drawable .news_item_adding_to_playlist)); } @Override public void onAnimationEnd(Animation animation) { icon.setImageDrawable(getResources().getDrawable(R.drawable .news_item_in_playlist)); } @Override public void onAnimationRepeat(Animation animation) { } }); icon.startAnimation(pulse); } private void showMinusAndFadeIcon(final View listItem) { final ImageView icon = (ImageView) listItem.findViewById(R.id .NewsItemIcon); if (icon == null) { Log.w(LOG_TAG, "Could not find the icon for list item: " + listItem); return; } Animation pulse = AnimationUtils.loadAnimation( NewsListActivity.this, R.anim.delete_and_fade ); pulse.setAnimationListener(new Animation.AnimationListener() { @Override public void onAnimationStart(Animation animation) { icon.setImageDrawable(getResources().getDrawable(R.drawable .news_item_deleting_from_playlist)); } @Override public void onAnimationEnd(Animation animation) { icon.setImageDrawable(getResources().getDrawable( R.drawable.speaker_icon )); // Need to update the list after animation so the correct item is animated if (flungStory != null) { PlaylistRepository playlistRepository = new PlaylistRepository(getApplicationContext(), getContentResolver()); playlistRepository.delete( playlistRepository.getPlaylistItemFromStoryId(flungStory.getId()) ); } } @Override public void onAnimationRepeat(Animation animation) { } }); icon.startAnimation(pulse); } private void animateListItemFling(final View listItem, int direction, final boolean isAdding) { Animation fling = AnimationUtils.loadAnimation( this, direction < 0 ? R.anim.left_fling : R.anim.right_fling ); fling.setAnimationListener(new Animation.AnimationListener() { @Override public void onAnimationEnd(Animation animation) { if (isAdding) { addAndPulseIcon(listItem); } else { showMinusAndFadeIcon(listItem); } } @Override public void onAnimationStart(Animation animation) { } @Override public void onAnimationRepeat(Animation animation) { } }); listItem.startAnimation(fling); } /** * Gets the NPR API URL used for looking up the items for the * list. This class may add startNum and numResults parameters * to the query provided by this method. * <p/> * The default implementation pulls the URL from the Intent's * EXTRA_QUERY_URL value. A subclass may override this to get * the URL another way. * * @return A URL for the NPR API. */ protected String getApiUrl() { return getIntent().getStringExtra(Constants.EXTRA_QUERY_URL); } /** * Gets a podcast RSS feed used for looking up the items for the list. This * will only be queried if getApiUrl returns null. No additional parameters * are added to the podcast list and all stories are shown immediately. * <p/> * The default implementation pulls the URL from the Intent's * EXTRA_PODCAST_URL value. * * @return A URL for a podcast RSS feed to be displayed as stories. */ protected String getPodcastUrl() { return getIntent().getStringExtra(Constants.EXTRA_PODCAST_URL); } @Override public CharSequence getMainTitle() { return description; } @Override public boolean isRefreshable() { return true; } @Override public void refresh() { listAdapter.clear(); addStories(); } // When first starting up, load the top news stories private void setDefaultIntent() { Map<String, String> params = new HashMap<String, String>(); params.put("id", "1001"); params.put("fields", ApiConstants.STORY_FIELDS); params.put("sort", "assigned"); String url = ApiConstants.instance().createUrl(ApiConstants.STORY_PATH, params); String grouping = null; Intent i = new Intent(this, NewsListActivity.class) .putExtra( Constants.EXTRA_SUBACTIVITY_ID, R.string.msg_main_subactivity_top_stories ) .putExtra(Constants.EXTRA_QUERY_URL, url) .putExtra(Constants.EXTRA_DESCRIPTION, "Top Stories") .putExtra(Constants.EXTRA_GROUPING, grouping) .putExtra(Constants.EXTRA_SIZE, 10); setIntent(i); } // Update every five seconds until we have a result private static final long UPDATE_SHORT_PERIOD = 5000L; // Update once a minute once we have a result private static final long UPDATE_LONG_PERIOD = 60000L; private Runnable updateTime = new Runnable() { public void run() { if (listAdapter == null) { handler.postDelayed(this, UPDATE_SHORT_PERIOD); return; } long lastUpdate = listAdapter.getLastUpdate(); if (lastUpdate < 0) { handler.postDelayed(this, UPDATE_SHORT_PERIOD); return; } String label = String.format(getString(R.string.msg_update_format), TimeUtils.formatMillis( System.currentTimeMillis() - lastUpdate, TimeUnit.DAYS, TimeUnit.MINUTES )); setTitleRight(label); handler.postDelayed(this, UPDATE_LONG_PERIOD); } }; private class PlaylistChangedReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { String operation = intent.getStringExtra(PlaylistRepository .PLAYLIST_CHANGE); if (operation != null && (operation.equals(PlaylistRepository.PLAYLIST_ITEM_REMOVED) || operation.equals(PlaylistRepository.PLAYLIST_CLEAR))) { listAdapter.notifyDataSetChanged(); } } } }