// 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: ArticleMarker.java,v 1.47 2007/09/19 15:55:00 spyromus Exp $ // package com.salas.bb.core; import com.salas.bb.domain.IArticle; import com.salas.bb.domain.IFeed; import com.salas.bb.domain.IGuide; import com.salas.bb.domain.prefs.UserPreferences; import com.salas.bb.utils.Constants; import com.salas.bb.utils.concurrency.ThreadExecutor; import com.salas.bb.views.feeds.IFeedDisplayListener; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.net.URL; import java.util.Timer; import java.util.TimerTask; import java.util.concurrent.Executor; import java.util.logging.Logger; /** * This class declares object which is intended to mark articles as read * under several conditions. Currently it supports three types of markings: * <ul> * <li>When user <b>leaves the channel</b> if <code>markReadWhenChangingChannels</code> property * set in user preferences.</li> * <li>When user <b>leaves the guide</b> and <code>markReadWhenChangingGuides</code> property * set in user preferences.</li> * <li>When <b>article selected more than number of seconds</b> specified by * <code>markReadAfterSeconds</code> property and when <code>markReadAfterDelay</code> * property is set in user preferences.</li> * </ul> * * There's an assumption that when changing guide, <code>channelSelected</code> * event always follows <code>guideSelected</code> event. */ public final class ArticleMarker extends ControllerAdapter { private static final Logger LOG = Logger.getLogger(ArticleMarker.class.getName()); private static final ArticleMarker INSTANCE = new ArticleMarker(); private static final int DEFAULT_INTERVAL = 5; private IFeed currentFeed; private IGuide currentGuideForGuide; private Timer timer; private MarkerTask task; private int markInterval; private boolean intervalMarkingEnabled; private FeedDisplayListener listener; /** Hidden singleton constructor. */ private ArticleMarker() { listener = new FeedDisplayListener(); timer = new Timer(true); markInterval = DEFAULT_INTERVAL; intervalMarkingEnabled = true; } /** * Returns instance of marker. * * @return instance. */ public static ArticleMarker getInstance() { return INSTANCE; } /** * Returns listener of feed view. * * @return listener. */ public FeedDisplayListener getFeedViewListener() { return listener; } /** * Invoked after application changes the channel. * * @param feed channel to which we are switching. */ public void feedSelected(IFeed feed) { if (GlobalModel.SINGLETON.getUserPreferences().isMarkReadWhenChangingChannels()) { final IFeed oldFeed = currentFeed; if (oldFeed != null && oldFeed != feed && oldFeed.getID() != -1) { MARK_EXECUTOR.execute(new MarkFeedAsReadOnSwitch(oldFeed)); } } currentFeed = feed; } /** * Invoked after application changes the guide. * * @param guide guide to with we have switched. */ public void guideSelected(final IGuide guide) { final IGuide oldGuide = currentGuideForGuide; UserPreferences prefs = GlobalModel.SINGLETON.getUserPreferences(); boolean markReadWhenChangingGuides = prefs.isMarkReadWhenChangingGuides(); if (markReadWhenChangingGuides && oldGuide != null && oldGuide != guide) { MARK_EXECUTOR.execute(new MarkGuideReadOnSwitch(oldGuide)); } currentGuideForGuide = guide; } private static final Executor MARK_EXECUTOR = new ThreadExecutor("Article Marker", 5000); /** * Returns current mark interval in seconds. * * @return seconds. */ public int getMarkInterval() { return markInterval; } /** * Sets new mark interval in seconds. Value becomes active on next article * selection. * * @param seconds interval in seconds. */ public void setMarkInterval(int seconds) { this.markInterval = seconds; } /** * Returns TRUE if interval marking is currently enabled. * * @return TRUE if enabled. */ public boolean isIntervalMarkingEnabled() { return intervalMarkingEnabled; } /** * Enables / disables interval marking. * * @param enabled TRUE to enable. */ public void setIntervalMarkingEnabled(boolean enabled) { intervalMarkingEnabled = enabled; } /** * Constructs and returns preference listener which will update interval marking * settings in accordance with changes to specified preferences. * * @param flagPreferenceName name of boolean flag preference. * @param intervalValuePreferenceName name of interval value preference. * * @return created and initialized preference change listener. */ public PropertyChangeListener getPreferencesListener(String flagPreferenceName, String intervalValuePreferenceName) { return new PreferencesListener(flagPreferenceName, intervalValuePreferenceName); } /** * Marker which marks currently selected article as read. */ private static class MarkerTask extends TimerTask { private IArticle article; /** * Constructs task. * * @param aArticle article to mark. */ public MarkerTask(IArticle aArticle) { article = aArticle; } /** * The action to be performed by this timer task. */ public void run() { GlobalModel model = GlobalModel.SINGLETON; if (article == model.getSelectedArticle()) { GlobalController.readArticles(true, model.getSelectedGuide(), model.getSelectedFeed(), article); } } } /** * Listener for changes of preferences. */ private class PreferencesListener implements PropertyChangeListener { private String flagPreferenceName = null; private String intervalValuePreferenceName = null; /** * Constructs listener. * * @param flagPreferenceName name of boolean flag preference. * @param intervalValuePreferenceName name of interval value preference. */ public PreferencesListener(String flagPreferenceName, String intervalValuePreferenceName) { this.flagPreferenceName = flagPreferenceName; this.intervalValuePreferenceName = intervalValuePreferenceName; } /** * This method gets called when a bound property is changed. * * @param evt A PropertyChangeEvent object describing the event source * and the property that has changed. */ public void propertyChange(PropertyChangeEvent evt) { final String prop = evt.getPropertyName(); if (flagPreferenceName != null && flagPreferenceName.equals(prop)) { setIntervalMarkingEnabled((Boolean)evt.getNewValue()); } else if (intervalValuePreferenceName != null && intervalValuePreferenceName.equals(prop)) { setMarkInterval((Integer)evt.getNewValue()); } } } /** * Thread that marks the feed as read on feed change. */ private static class MarkFeedAsReadOnSwitch implements Runnable { private final IFeed feed; public MarkFeedAsReadOnSwitch(IFeed aFeed) { feed = aFeed; } public void run() { feed.setRead(true); } } /** * Marks guide as read on switch. */ private static class MarkGuideReadOnSwitch implements Runnable { private final IGuide guide; public MarkGuideReadOnSwitch(IGuide aGuide) { guide = aGuide; } public void run() { guide.setRead(true); } } /** * Listens to the view and schedules marking of articles as read. */ private class FeedDisplayListener implements IFeedDisplayListener { /** * Invoked when user selects article or article is selected as result of direct invocation * of {@link com.salas.bb.views.feeds.IFeedDisplay#selectArticle(com.salas.bb.domain.IArticle)} * method. * * @param lead lead article. * @param selectedArticles all selected articles. */ public void articleSelected(IArticle lead, IArticle[] selectedArticles) { // TODO: group marking of articles as read when multiple selected? if (task != null) task.cancel(); if (lead != null && intervalMarkingEnabled && !lead.isRead()) { task = new MarkerTask(lead); timer.schedule(task, getMarkInterval() * Constants.MILLIS_IN_SECOND); } } /** * Invoked when user clicks on some link at the article text or header. The expected * behaviour is openning the link in browser. * * @param link link clicked. */ public void linkClicked(URL link) { } /** * Invoked when user hovers some link with mouse pointer. * * @param link link hovered or <code>NULL</code> if previously hovered link is no longer * hovered. */ public void linkHovered(URL link) { } /** * Invoked when user clicks on some quick-link to the other feed. * * @param feed feed to select. */ public void feedJumpLinkClicked(IFeed feed) { } /** Invoked when the user made something to zoom content in. */ public void onZoomIn() { } /** Invoked when the user made something to zoom the content out. */ public void onZoomOut() { } } }