// BlogBridge -- RSS feed reader, manager, and web based service
// Copyright (C) 2002-2006 by R. Pito Salas
//
// This program is free software; you can redistribute it and/or modify it under
// the terms of the GNU General Public License as published by the Free Software Foundation;
// either version 2 of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
// without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
// See the GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License along with this program;
// if not, write to the Free Software Foundation, Inc., 59 Temple Place,
// Suite 330, Boston, MA 02111-1307 USA
//
// Contact: R. Pito Salas
// mailto:pitosalas@users.sourceforge.net
// More information: about BlogBridge
// http://www.blogbridge.com
// http://sourceforge.net/projects/blogbridge
//
// $Id: GlobalModel.java,v 1.140 2008/04/08 08:06:18 spyromus Exp $
//
package com.salas.bb.core;
import com.salas.bb.domain.*;
import com.salas.bb.domain.events.FeedRemovedEvent;
import com.salas.bb.domain.prefs.StarzPreferences;
import com.salas.bb.domain.prefs.UserPreferences;
import com.salas.bb.domain.utils.DomainAdapter;
import com.salas.bb.domain.utils.DomainEventsListener;
import com.salas.bb.service.ServicePreferences;
import com.salas.bb.utils.dnd.DNDListContext;
import com.salas.bb.views.settings.DefaultFRS;
import com.salas.bb.views.settings.FeedRenderingSettings;
import com.salas.bb.views.settings.RenderingManager;
import javax.swing.*;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.Date;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.prefs.Preferences;
/**
* Root of the model of the whole application. The key state that we keep are the various channel
* guides, which may be displayed in the main window. This is a singleton object. N.B. This class is
* persisted as XML using XMLEncoder. XMLEncoder's default behavior is that it will write out all
* bean properties. Therefore the part of this object's state which is available as a Bean Propertyu
* is exactly what will be written out. This is subtle so be careful if you play with getXXX and
* setXXX methods which is how by default you can tell what's a bean property and what is not.
*/
public class GlobalModel
{
private static final Logger LOG = Logger.getLogger(GlobalModel.class.getName());
/**
* Return the one and only GlobalModel. Tricky: during the writing out of the state on disk a
* copy of GlobalModel is temporarily created
*/
public static GlobalModel SINGLETON;
private final UserPreferences userPreferences;
private final ServicePreferences servicePreferences;
private final StarzPreferences starzPreferences;
private final FeedRenderingSettings globalRenderingSettings;
private GuidesSet guidesSet;
private IGuide selectedGuide;
private IFeed selectedFeed;
private IArticle selectedArticle;
private IArticle[] selectedArticles;
private final GuideModel guideModel;
/** A model for supporting computation of visible feeds. */
private GuideModel vfHelperModel;
private boolean blockChannelUpdated;
// We use this info block to record the fact of selecteed feed change not passing feed change
// event to the model. Doing this we avoid refiltering and resorting basing on new feed
// state, which may result in change of feed position or visibility. These changes break
// the human-expected navigation path and should be avoided while feed is selected. Later,
// when user will select another feed this feed will be reported as changed to the model.
private final FeedChangeInfo selectedFeedChangeInfo;
/** Listener of all domain events. */
private final DomainListener domainListener;
/**
* Holds the mapping between guide (key) and feed object.
*/
private Map<IGuide, IFeed> selectedFeeds;
/** View mode value model. */
private ViewModeValueModel viewModeValueModel;
/** View type value model. */
private ViewTypeValueModel viewTypeValueModel;
/**
* Construct the global state. After construction, the GlobalModel has an empty ChannelGuideSet.
* Note that this constructor is called either directly (new, when the state is not coming from
* the persistent state) or via the XMLDecoder call in GlobalController.UnPersistAsXML which
* instantiates a GlobalModel from its persistent state. In either case the GlobalModel.INSTANCE
* static contains a reference to the GlobalModel object.
*
* @param scoresCalculator scores calculator to use.
*/
public GlobalModel(ScoresCalculator scoresCalculator)
{
this(scoresCalculator, true);
}
/**
* Construct the global state. After construction, the GlobalModel has an empty ChannelGuideSet.
* Note that this constructor is called either directly (new, when the state is not coming from
* the persistent state) or via the XMLDecoder call in GlobalController.UnPersistAsXML which
* instantiates a GlobalModel from its persistent state. In either case the GlobalModel.INSTANCE
* static contains a reference to the GlobalModel object.
*
* @param scoresCalculator scores calculator to use.
* @param full TRUE to perform full initialization. FALSE to create only
* this model object.
*/
public GlobalModel(ScoresCalculator scoresCalculator, boolean full)
{
userPreferences = new UserPreferences();
servicePreferences = new ServicePreferences();
starzPreferences = new StarzPreferences();
globalRenderingSettings = new FeedRenderingSettings();
globalRenderingSettings.setParent(new DefaultFRS());
if (full)
{
domainListener = new DomainListener();
guideModel = new GuideModel(scoresCalculator, FeedDisplayModeManager.getInstance());
vfHelperModel = new GuideModel(scoresCalculator, FeedDisplayModeManager.getInstance());
RenderingManager.setGlobalSettings(globalRenderingSettings);
selectedFeedChangeInfo = new FeedChangeInfo();
selectedFeeds = new IdentityHashMap<IGuide, IFeed>();
// When some property affecting visibility changes we
// mark the selected feed info as changed to update its cell
// when it gets unselected. It can potentially become invisible.
userPreferences.addPropertyChangeListener(new PropertyChangeListener()
{
public void propertyChange(PropertyChangeEvent evt)
{
if (UserPreferences.FEED_VISIBILITY_PROPERTIES.contains(evt.getPropertyName()))
{
selectedFeedChangeInfo.registerChanged();
}
}
});
} else
{
selectedFeedChangeInfo = null;
guideModel = null;
domainListener = null;
}
viewModeValueModel = new ViewModeValueModel(globalRenderingSettings);
viewTypeValueModel = new ViewTypeValueModel();
setGuidesSet(new GuidesSet());
selectedGuide = null;
blockChannelUpdated = false;
}
/**
* Returns the view mode value model.
*
* @return view mode value mode.
*/
public ViewModeValueModel getViewModeValueModel()
{
return viewModeValueModel;
}
/**
* Returns the view type value model.
*
* @return view type value mode.
*/
public ViewTypeValueModel getViewTypeValueModel()
{
return viewTypeValueModel;
}
/**
* Returns current highlight calculator to be used.
*
* @return highlights calculator.
*/
public HighlightsCalculator getHighlightsCalculator()
{
return GlobalController.SINGLETON.getHighlightsCalculator();
}
/**
* Returns calculator of channel score.
*
* @return calculator.
*/
public ScoresCalculator getScoreCalculator()
{
return GlobalController.SINGLETON.getScoreCalculator();
}
/**
* Returns channels model.
*
* @return model.
*/
public GuideModel getGuideModel()
{
return guideModel;
}
/**
* Returns visible feeds helper mode.
*
* @param aGuide target guide.
*
* @return model.
*/
private GuideModel getVFHelperModel(IGuide aGuide)
{
int starz = userPreferences.getGoodChannelStarz() - 1;
int prSortOrder = userPreferences.getSortByClass1();
int scSortOrder = userPreferences.getSortByClass2();
boolean isPrSortOrderReversed = userPreferences.isReversedSortByClass1();
boolean isScSortOrderReversed = userPreferences.isReversedSortByClass2();
boolean isSortingEnabled = userPreferences.isSortingEnabled();
vfHelperModel.setOptions(starz, prSortOrder, isPrSortOrderReversed,
scSortOrder, isScSortOrderReversed, isSortingEnabled);
vfHelperModel.setGuide(aGuide);
return vfHelperModel;
}
/**
* Returns <code>TRUE</code> if a feed is currently visible on the screen (in the selected guide).
*
* @param feed feed to check.
*
* @return <code>TRUE</code> if a feed is currently visible on the screen (in the selected guide).
*/
public boolean isCurrentlyVisible(IFeed feed)
{
return guideModel.isPresent(feed);
}
/**
* After reading this in from persistent state, initialize parts of the state of GlobalModel
* which do not get persisted.
*/
public void initTransientState()
{
}
/**
* Update the Model with what guide is currently selected.
*
* @param guide new guide selection.
*/
public void setSelectedGuide(final IGuide guide)
{
selectedGuide = guide;
// EDT !!!
if (guideModel != null) guideModel.setGuide(guide);
if (guide != null)
{
selectedFeed = selectedFeeds.get(guide);
if (selectedFeed == null && guideModel != null && guideModel.getSize() > 0)
{
selectedFeed = (IFeed)guideModel.getElementAt(0);
}
} else selectedFeed = null;
}
/**
* Returns currently selected guide.
*
* @return selected guide.
*/
public IGuide getSelectedGuide()
{
return selectedGuide;
}
/**
* Returns selected article.
*
* @return selected article.
*/
public IArticle getSelectedArticle()
{
return selectedArticle;
}
/**
* Sets selected article.
*
* @param aArticle selected article.
*/
public void setSelectedArticle(IArticle aArticle)
{
selectedArticle = aArticle;
}
/**
* Returns the list of currently selected articles.
*
* @return articles.
*/
public IArticle[] getSelectedArticles()
{
return selectedArticles;
}
/**
* Sets selected articles.
*
* @param aSelectedArticles articles.
*/
public void setSelectedArticles(IArticle[] aSelectedArticles)
{
selectedArticles = aSelectedArticles;
}
/**
* Selects the feed.
*
* @param feed feed to select.
*/
public void setSelectedFeed(final IFeed feed)
{
if (feed == null || isSelectable(feed))
{
selectedFeed = feed;
if (feed != null)
{
IGuide selectedGuide = getSelectedGuide();
selectedFeeds.put(selectedGuide, feed);
feed.setLastVisitTime(System.currentTimeMillis());
feed.setViews(feed.getViews() + 1);
}
viewModeValueModel.setFeed(feed);
viewTypeValueModel.setFeed(feed);
}
firePreviousFeedChanged();
synchronized (selectedFeedChangeInfo)
{
selectedFeedChangeInfo.reset(feed);
}
}
private void firePreviousFeedChanged()
{
IFeed feed = null;
synchronized (selectedFeedChangeInfo)
{
if (selectedFeedChangeInfo.changed)
{
feed = selectedFeedChangeInfo.feed;
}
}
if (feed != null)
{
final IFeed feed1 = feed;
SwingUtilities.invokeLater(new Runnable()
{
public void run()
{
guideModel.contentsChangedAt(feed1);
}
});
}
}
/**
* Returns <code>true</code> if feed is selectable.
*
* @param feed feed to check.
*
* @return result of check.
*/
public boolean isSelectable(final IFeed feed)
{
IGuide selectedGuide = getSelectedGuide();
return feed == null ||
(selectedGuide != null &&
feed.belongsTo(selectedGuide) &&
selectedGuide.indexOf(feed) != -1);
}
/**
* Returns selected feed.
*
* @return selected feed.
*/
public IFeed getSelectedFeed()
{
return selectedFeed;
}
/**
* Sets channel guide set.
*
* @param set channel guide set.
*/
public void setGuidesSet(final GuidesSet set)
{
// Connect model domain listener to the new set
new DomainEventsListener(set).addDomainListener(domainListener);
guidesSet = set;
}
/**
* Returns the channel guide set.
*
* @return set.
*/
public GuidesSet getGuidesSet()
{
return guidesSet;
}
/**
* Sets new singleton.
*
* @param model new singleton.
*/
public static void setSINGLETON(final GlobalModel model)
{
SINGLETON = model;
}
/**
* Called to clean up model related resources, kill processes and save XML state just before
* exiting.
*/
public void prepareForApplicationExit()
{
}
/**
* Returns user preferences object.
*
* @return the user preferences object.
*/
public UserPreferences getUserPreferences()
{
return userPreferences;
}
/**
* Returns service preferences.
*
* @return preferences.
*/
public ServicePreferences getServicePreferences()
{
return servicePreferences;
}
/**
* Returns global rendering settings.
*
* @return global rendering settings.
*/
public FeedRenderingSettings getGlobalRenderingSettings()
{
return globalRenderingSettings;
}
/**
* Returns Starz service preferences.
*
* @return preferences object.
*/
public StarzPreferences getStarzPreferences()
{
return starzPreferences;
}
/**
* Notifies model about change in selected feed if containing guide is currently displayed.
*
* @param feed changed feed.
*/
public void feedUpdated(IFeed feed)
{
if (blockChannelUpdated) return;
if (LOG.isLoggable(Level.FINE)) LOG.fine("Feed updated " + feed);
if (selectedFeedChangeInfo.feed == feed)
{
selectedFeedChangeInfo.registerChanged();
SwingUtilities.invokeLater(new FireFeedUpdated(feed, true));
} else
{
SwingUtilities.invokeLater(new FireFeedUpdated(feed, false));
}
}
/**
* Called when the initial loading of the feeds from database starts.
*/
protected void loadingStarted()
{
blockChannelUpdated = true;
}
/**
* Called when the initial loading of the feeds finishes.
*/
protected void loadingFinished()
{
if (blockChannelUpdated)
{
blockChannelUpdated = false;
// Doing update of model from within EDT
SwingUtilities.invokeLater(new Runnable()
{
public void run()
{
getHighlightsCalculator().invalidateAll();
getScoreCalculator().invalidateAll();
guideModel.fullRebuild();
}
});
}
}
/**
* Given a guide, return a list of feeds which would be displayed
* in that guide given current filter settings and options.
* Uses the existing guide model if it corresponds to the requested guide,
* otherwise creates a temporary GuideModel instance.
* @param guide Guide to look in.
* @return array of visible feeds for that guide.
*/
public IFeed[] getVisibleFeeds(IGuide guide)
{
GuideModel model = (guide == selectedGuide) ? guideModel : getVFHelperModel(guide);
// !!! EDT
int numFeeds = model.getSize();
IFeed[] retFeeds = new IFeed[numFeeds];
for (int i = 0; i < numFeeds; ++i) retFeeds[i] = (IFeed) model.getElementAt(i);
return retFeeds;
}
/**
* Returns the number of unread articles in visible data feeds only within the given guide.
*
* @param aGuide guide.
*
* @return number of unread articles.
*/
public int getUnreadArticlesCount(IGuide aGuide)
{
int count = 0;
GuideModel mdl = getVFHelperModel(aGuide);
for (int i = 0; i < mdl.getSize(); i++)
{
Object item = mdl.getElementAt(i);
if (item instanceof DataFeed)
{
count += ((DataFeed)item).getUnreadArticlesCount();
}
}
return count;
}
/**
* Updates the last update time of preferences.
*/
public static void touchPreferences()
{
SINGLETON.getUserPreferences().setLastUpdateTime(new Date());
}
/** Feed changes information block. Used to store information about selected feed changes. */
private static class FeedChangeInfo
{
private IFeed feed;
private boolean changed;
/**
* Resets the information and sets new feed.
*
* @param aFeed new feed.
*/
public synchronized void reset(IFeed aFeed)
{
feed = aFeed;
changed = false;
}
/**
* Register change of the feed.
*/
public synchronized void registerChanged()
{
changed = true;
}
}
// ------------------------------------------------------------------------
// Preferences
// ------------------------------------------------------------------------
/**
* Stores preferences in the object.
*
* @param prefs preference storage.
*/
public void storePreferences(Preferences prefs)
{
userPreferences.storeIn(prefs);
servicePreferences.storeIn(prefs);
globalRenderingSettings.storeIn(prefs);
starzPreferences.storeIn(prefs);
}
/**
* Restores preferences.
*
* @param prefs preference storage.
*/
public void restorePreferences(Preferences prefs)
{
userPreferences.restoreFrom(prefs);
servicePreferences.restoreFrom(prefs);
globalRenderingSettings.restoreFrom(prefs);
starzPreferences.restoreFrom(prefs);
}
// ------------------------------------------------------------------------
// Helper classes
// ------------------------------------------------------------------------
/** Fires feed update event. */
private class FireFeedUpdated implements Runnable
{
private final IFeed feed;
private final boolean fireEvent;
public FireFeedUpdated(IFeed aFeed, boolean event)
{
feed = aFeed;
fireEvent = event;
}
public void run()
{
if (fireEvent)
{
synchronized (guideModel)
{
int index = guideModel.indexOf(feed);
if (index > -1) guideModel.fireContentsChanged(guideModel, index, index);
}
if (getSelectedFeed() == feed) GlobalController.SINGLETON.getMainFrame().updateTitle(feed);
} else
{
guideModel.contentsChangedAt(feed);
}
}
}
/**
* Listener of domain events.
*/
private class DomainListener extends DomainAdapter
{
private static final String THREAD_NAME_ARTICLE_SPECIAL_FUNCTIONS = "Article Special Functions";
private static final String THREAD_NAME_UPDATE_SCORES = "Update Scores";
/**
* Invoked when new guide has been added to the set.
*
* @param set guides set.
* @param guide added guide.
* @param lastInBatch <code>TRUE</code> when this is the last even in batch.
*/
public void guideAdded(GuidesSet set, IGuide guide, boolean lastInBatch)
{
synchronized (guide)
{
int count = guide.getFeedsCount();
for (int i = 0; i < count; i++) feedAdded(guide, guide.getFeedAt(i));
}
}
/**
* Invoked when the guide has been removed from the set.
*
* @param set guides set.
* @param guide removed guide.
* @param index old guide index.
*/
public void guideRemoved(GuidesSet set, IGuide guide, int index)
{
synchronized (guide)
{
int count = guide.getFeedsCount();
for (int i = 0; i < count; i++) feedRemovedCommon(guide, guide.getFeedAt(i));
}
// Release the guide being deleted from Visible Feeds helper model
selectedFeeds.remove(guide);
if (vfHelperModel != null && vfHelperModel.getCurrentGuide() == guide)
{
vfHelperModel.setGuide(null);
}
// Release the guide being deleted from the DND list context
if (DNDListContext.getDestination() == guide) DNDListContext.setDestination(null);
}
// -----------------------------------------------------------------------------------------
// Feeds Stuff
// -----------------------------------------------------------------------------------------
/**
* Invoked when new feed has been added to the guide.
*
* @param guide parent guide.
* @param feed added feed.
*/
public void feedAdded(IGuide guide, IFeed feed)
{
IArticle[] articles = feed.getArticles();
for (IArticle article : articles) articleAdded(feed, article);
if (selectedGuide == guide)
{
final int index = guide.indexOf(feed);
SwingUtilities.invokeLater(new Runnable()
{
public void run()
{
guideModel.feedsAdded(index, index);
}
});
}
}
/**
* Invoked when the feed has been removed from the guide.
*
* @param event feed removal event.
*/
public void feedRemoved(FeedRemovedEvent event)
{
IGuide guide = event.getGuide();
final IFeed feed = event.getFeed();
// Do common processing
feedRemovedCommon(guide, feed);
if (selectedGuide == guide && event.isLastEvent())
{
final int visibleIndex = guideModel.indexOf(feed);
// Recalculation of model should happen in EDT
SwingUtilities.invokeLater(new Runnable()
{
public void run()
{
guideModel.fullRebuild();
if (selectedFeed == feed)
{
IFeed feedToSelect = null;
int count = guideModel.getSize();
if (visibleIndex < count && count > 0)
{
feedToSelect = (IFeed)guideModel.getElementAt(visibleIndex == -1
? 0 : visibleIndex);
} else if (count > 0)
{
feedToSelect = (IFeed)guideModel.getElementAt(count - 1);
}
GlobalController.SINGLETON.selectFeed(feedToSelect);
}
}
});
}
}
/**
* Invoked when a feed is moved from one position to another.
*
* @param guide source guide.
* @param feed feed moved.
* @param oldPosition old position.
* @param newPosition new position.
*/
public void feedRepositioned(final IGuide guide, final IFeed feed, int oldPosition, int newPosition)
{
if (guideModel.getCurrentGuide() == guide)
{
SwingUtilities.invokeLater(new Runnable()
{
public void run()
{
// We reselect guide to let GuideModel reload the list
// of feeds that is cached at the moment.
guideModel.setGuide(guide);
}
});
}
}
/**
* Common handling of removed feed event. Unregisteres listener from feed,
* removes the associated selection key and clears the scores.
*
* @param guide guide the feed was removed from.
* @param feed removed feed.
*/
void feedRemovedCommon(IGuide guide, IFeed feed)
{
// Remove selected feed if it's equal to the one we just removed
IFeed selectedFeed = selectedFeeds.get(guide);
if (selectedFeed == feed) selectedFeeds.remove(guide);
// Remove highlights only if this feed is not assigned anywhere else
if (feed.getParentGuides().length == 0)
{
getHighlightsCalculator().feedRemoved(feed);
getScoreCalculator().feedRemoved(feed);
}
}
/**
* Special functions for new articles.
*
* @param article new article.
*/
private void newArticleSpecialFunctions(final IArticle article)
{
final DataFeed feed = (DataFeed)article.getFeed();
final boolean autoDiscovery = feed.isAutoFeedsDiscovery();
// Span new thread only if at least one of the functions is required.
if (autoDiscovery)
{
Thread thread = new Thread(THREAD_NAME_ARTICLE_SPECIAL_FUNCTIONS)
{
public void run()
{
// Discover links in new articles
if (autoDiscovery)
{
GlobalController.SINGLETON.discoverFeedsIn(article, feed);
}
}
};
thread.start();
}
}
/**
* Updates the scores (highlights and overall) of the feed.
*
* @param feed feed which scores to update.
* @param updateHighlights TRUE to update highlights.
*/
private void updateScores(final IFeed feed, final boolean updateHighlights)
{
Thread thread = new Thread(THREAD_NAME_UPDATE_SCORES)
{
public void run()
{
// Submit invalidation request
HighlightsCalculator hcalc = null;
if (updateHighlights)
{
hcalc = getHighlightsCalculator();
hcalc.invalidateFeed(feed);
}
ScoresCalculator scalc = getScoreCalculator();
scalc.invalidateFeed(feed);
// Wait for calc finish
if (updateHighlights) hcalc.getHighlightsCount(feed);
if (feed instanceof DirectFeed) scalc.calcBlogStarzScore(feed);
// Repaint feed
feedUpdated(feed);
}
};
thread.start();
}
/**
* Called when information in feed changed.
*
* @param feed feed.
* @param property property of the feed.
* @param oldValue old property value.
* @param newValue new property value.
*/
public void propertyChanged(IFeed feed, String property, Object oldValue, Object newValue)
{
if (!feed.belongsTo(selectedGuide)) return;
boolean repaintNecessary = IFeed.PROP_TITLE.equals(property) ||
DirectFeed.PROP_RATING.equals(property) ||
DirectFeed.PROP_DISABLED.equals(property) ||
IFeed.PROP_UNREAD_ARTICLES_COUNT.equals(property) ||
IFeed.PROP_PROCESSING.equals(property) ||
IFeed.PROP_INVALIDNESS_REASON.equals(property) ||
SearchFeed.PROP_QUERY.equals(property) ||
SearchFeed.PROP_ARTICLES_LIMIT.equals(property);
boolean updateScore = false;
boolean updateHighlights = false;
if (IFeed.PROP_PROCESSING.equals(property) && !(Boolean)newValue)
{
// processing finished -- invalidate feed
updateScore = true;
updateHighlights = true;
} else if (IFeed.PROP_CLICKTHROUGHS.equals(property))
{
// the number of clickthroughs has changed
if (ScoresCalculator.registerMaxClickthroughs(feed.getClickthroughs()))
updateFeedsWithNonZeroClickthroughs();
else updateScore = true;
} else if (IFeed.PROP_VIEWS.equals(property))
{
// the number of views has changed
if (ScoresCalculator.registerMaxFeedViews(feed.getViews()))
updateFeedsWithNonZeroViews();
else updateScore = true;
}
if (updateScore) updateScores(feed, updateHighlights);
if (repaintNecessary) feedUpdated(feed);
}
private void updateFeedsWithNonZeroViews()
{
ScoresCalculator sc = getScoreCalculator();
List<IFeed> feeds = getGuidesSet().getFeeds();
for (IFeed feed : feeds)
{
if (feed.getViews() != 0)
{
sc.invalidateFeed(feed);
feedUpdated(feed);
}
}
}
private void updateFeedsWithNonZeroClickthroughs()
{
ScoresCalculator sc = getScoreCalculator();
List<IFeed> feeds = getGuidesSet().getFeeds();
for (IFeed feed : feeds)
{
if (feed.getClickthroughs() != 0)
{
sc.invalidateFeed(feed);
feedUpdated(feed);
}
}
}
// -----------------------------------------------------------------------------------------
// Articles
// -----------------------------------------------------------------------------------------
/**
* Called when some article is added to the feed.
*
* @param feed feed.
* @param article article.
*/
public void articleAdded(final IFeed feed, final IArticle article)
{
if (GlobalController.SINGLETON.isInitializationFinished() && feed instanceof DataFeed)
newArticleSpecialFunctions(article);
}
}
}