// 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 $
//
package com.salas.bb.core;
import com.jgoodies.uif.util.ResourceUtils;
import com.salas.bb.domain.*;
import com.salas.bb.domain.prefs.UserPreferences;
import com.salas.bb.domain.utils.DomainAdapter;
import com.salas.bb.utils.Sound;
import com.salas.bb.utils.i18n.Strings;
import com.salas.bb.utils.notification.NotificationArea;
import javax.swing.*;
import java.net.URL;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.TimerTask;
import java.util.logging.Logger;
/**
* Collects events, groups them and sends out notifications.
*/
public class EventsNotifier extends DomainAdapter
{
private static final Logger LOG = Logger.getLogger(EventsNotifier.class.getName());
/** Minimum time to pass after the last new feed/article evern to report the whole list. */
private static final long MINIMUM_PAUSE = 10000L;
/** The period between pause checks. */
private static final long CHECK_PERIOD = MINIMUM_PAUSE / 5L;
/** The name of the application. */
private static final String APP_NAME = "BlogBridge";
/** New feeds and articles event name. */
private static final String EVENT_NEW_FEEDS_AND_ARTICLES = Strings.message("event.new.feeds.and.articles");
/** The timer we use to schedule checks. */
private final java.util.Timer timer;
/** The frame we look after. If it's focused, we don't do anything. */
private JFrame frame;
/** The counter of new articles to be reported. */
private int newArticles;
/** The counter of new feeds across the reading lists to be reported. */
private int newFeeds;
/** The list of guides updated with new articles and feeds. */
private java.util.List<String> updatedGuides;
/** The time of last new feed/article event. */
private long lastEventTime;
/** Current timer task. */
private CheckTimerTask task;
/** User preferences. There will be no notifications without this object set. */
private UserPreferences prefs;
/** Resource ID for the new article sound. */
private String soundResourceID;
/**
* Creates notifier.
*/
public EventsNotifier()
{
String[] events = new String[] { EVENT_NEW_FEEDS_AND_ARTICLES };
URL bigImage = null;
try
{
bigImage = ResourceUtils.getURL(ResourceUtils.getString("application.64.icon"));
} catch (NullPointerException e)
{
// We don't care about images we couldn't initialize
LOG.warning(Strings.error("failed.to.load.image"));
}
NotificationArea.init(APP_NAME, events, bigImage);
timer = new java.util.Timer();
updatedGuides = new ArrayList<String>();
soundResourceID = null;
resetStats();
}
/**
* Sets ID of the resource.
*
* @param resourceID resource.
*/
public void setSoundResourceID(String resourceID)
{
this.soundResourceID = resourceID;
}
/**
* Registers the frame we depend on.
*
* @param frame frame.
*/
public void setFrame(JFrame frame)
{
this.frame = frame;
}
/**
* Sets user preferences to use.
*
* @param prefs preferences.
*/
public void setUserPreferences(UserPreferences prefs)
{
this.prefs = prefs;
}
/**
* Invoked when new article has been added to the feed.
*
* @param feed feed.
* @param article article.
*/
public void articleAdded(IFeed feed, IArticle article)
{
if (shouldNotify(feed))
{
synchronized (this)
{
newArticles++;
updateGuidesList(feed);
lastEventTime = System.currentTimeMillis();
startTimer();
}
}
}
/**
* Returns <code>TRUE</code> if events in this feed should be reported. The feed
* can be in no guides -- no notifications. If the feed is in one or more guides
* the notification should be sent if only there's at least one guide having
* notifications allowed.
*
* @param feed feed to check.
*
* @return <code>TRUE</code> if should be reported.
*/
private boolean shouldNotify(IFeed feed)
{
boolean should = false;
if (prefs != null && prefs.isNotificationsEnabled() && !frame.isFocused() &&
(!(feed instanceof DirectFeed) || !((DirectFeed)feed).isDisabled()))
{
IGuide[] parents = feed.getParentGuides();
for (int i = 0; !should && i < parents.length; i++)
{
should = parents[i].isNotificationsAllowed();
}
}
return should;
}
/**
* Invoked when new feed has been added to the reading list.
*
* @param list reading list the feed was added to.
* @param feed added feed.
*/
public void feedAdded(ReadingList list, IFeed feed)
{
if (shouldNotify(list))
{
synchronized (this)
{
newFeeds++;
updateGuidesList(feed);
lastEventTime = System.currentTimeMillis();
startTimer();
}
}
}
/**
* Tells if the notification for new feed in the list should be sent or no.
*
* @param list list the new feed appeared in.
*
* @return <code>TRUE</code> if notification is necessary.
*/
private boolean shouldNotify(ReadingList list)
{
boolean should = false;
if (prefs != null && prefs.isNotificationsEnabled() && !frame.isFocused())
{
IGuide parent = list.getParentGuide();
should = parent != null && parent.isNotificationsAllowed();
}
return should;
}
/**
* Registers the guides updated as the result of the feed update.
*
* @param feed feed.
*/
private void updateGuidesList(IFeed feed)
{
IGuide[] guides = feed.getParentGuides();
for (IGuide guide : guides)
{
String title = guide.getTitle();
if (!updatedGuides.contains(title)) updatedGuides.add(title);
}
}
/**
* Starts timer, which fires every {@link #CHECK_PERIOD} number of ms and
* checks if {@link #MINIMUM_PAUSE} ms passed since the last event. If desired
* time ellapsed, it fires message balloon through notification framework. If
* there's nothing to report, the timer stops itself.
*/
private void startTimer()
{
if (task == null)
{
task = new CheckTimerTask();
timer.schedule(task, CHECK_PERIOD, CHECK_PERIOD);
}
}
/**
* Fires notification.
*/
private void fireNotification()
{
if (prefs != null && prefs.isNotificationsEnabled())
{
String message = "";
boolean articles = newArticles > 0;
boolean feeds = newFeeds > 0;
if (articles)
{
message += newArticles == 1
? Strings.message("event.1.article")
: MessageFormat.format(Strings.message("event.n.articles"), newArticles);
if (feeds) message += " " + Strings.message("event.and");
}
if (feeds) message += newFeeds == 1
? Strings.message("event.1.feed")
: MessageFormat.format(Strings.message("event.n.feeds"), newFeeds);
message += " ";
message += (newArticles + newFeeds > 1)
? Strings.message("event.have.been.added.to")
: Strings.message("event.has.been.added.to");
message += " ";
if (updatedGuides.size() == 1)
{
message += MessageFormat.format(Strings.message("event.guide.0"), updatedGuides.get(0));
} else
{
message += MessageFormat.format(Strings.message("event.n.guides"),
updatedGuides.size());
}
NotificationArea.setAppIconTempVisible(true);
NotificationArea.showMessage(EVENT_NEW_FEEDS_AND_ARTICLES, message);
if (articles && soundResourceID != null && prefs.isSoundOnNewArticles()) Sound.play(soundResourceID);
}
resetStats();
}
/**
* Resets counters and stats.
*/
private void resetStats()
{
updatedGuides.clear();
newArticles = 0;
newFeeds = 0;
}
/**
* Checker for events and firer of notifications.
*/
private class CheckTimerTask extends TimerTask
{
private boolean isNothingToReport()
{
return newArticles == 0 && newFeeds == 0;
}
/**
* The action to be performed by this timer task.
*/
public void run()
{
synchronized (EventsNotifier.this)
{
if (!isNothingToReport() && System.currentTimeMillis() - lastEventTime >= MINIMUM_PAUSE)
{
fireNotification();
}
if (isNothingToReport())
{
task = null;
cancel();
}
}
}
}
}