Package org.rssowl.ui.internal.notifier

Source Code of org.rssowl.ui.internal.notifier.NotificationService

/*   **********************************************************************  **
**   Copyright notice                                                       **
**                                                                          **
**   (c) 2005-2009 RSSOwl Development Team                                  **
**   http://www.rssowl.org/                                                 **
**                                                                          **
**   All rights reserved                                                    **
**                                                                          **
**   This program and the accompanying materials are made available under   **
**   the terms of the Eclipse Public License v1.0 which accompanies this    **
**   distribution, and is available at:                                     **
**   http://www.rssowl.org/legal/epl-v10.html                               **
**                                                                          **
**   A copy is found in the file epl-v10.html and important notices to the  **
**   license from the team is found in the textfile LICENSE.txt distributed **
**   in this package.                                                       **
**                                                                          **
**   This copyright notice MUST APPEAR in all copies of the file!           **
**                                                                          **
**   Contributors:                                                          **
**     RSSOwl Development Team - initial API and implementation             **
**                                                                          **
**  **********************************************************************  */

package org.rssowl.ui.internal.notifier;

import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.swt.graphics.RGB;
import org.eclipse.swt.widgets.Shell;
import org.rssowl.core.Owl;
import org.rssowl.core.internal.persist.pref.DefaultPreferences;
import org.rssowl.core.persist.IBookMark;
import org.rssowl.core.persist.IFolder;
import org.rssowl.core.persist.INews;
import org.rssowl.core.persist.ISearchCondition;
import org.rssowl.core.persist.ISearchMark;
import org.rssowl.core.persist.dao.DynamicDAO;
import org.rssowl.core.persist.event.BookMarkAdapter;
import org.rssowl.core.persist.event.BookMarkEvent;
import org.rssowl.core.persist.event.BookMarkListener;
import org.rssowl.core.persist.event.FolderAdapter;
import org.rssowl.core.persist.event.FolderEvent;
import org.rssowl.core.persist.event.FolderListener;
import org.rssowl.core.persist.event.NewsAdapter;
import org.rssowl.core.persist.event.NewsEvent;
import org.rssowl.core.persist.event.NewsListener;
import org.rssowl.core.persist.event.SearchMarkAdapter;
import org.rssowl.core.persist.event.SearchMarkEvent;
import org.rssowl.core.persist.event.SearchMarkListener;
import org.rssowl.core.persist.pref.IPreferenceScope;
import org.rssowl.core.util.BatchedBuffer;
import org.rssowl.core.util.CoreUtils;
import org.rssowl.ui.internal.ApplicationWorkbenchAdvisor;
import org.rssowl.ui.internal.ApplicationWorkbenchWindowAdvisor;
import org.rssowl.ui.internal.Controller;
import org.rssowl.ui.internal.OwlUI;
import org.rssowl.ui.internal.dialogs.preferences.NotifierPreferencesPage;
import org.rssowl.ui.internal.util.JobRunner;

import java.util.Collection;
import java.util.EnumSet;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.ConcurrentHashMap;

/**
* The <code>NotificationService</code> listens on News being downloaded and
* opens the <code>NotificationPopup</code> to show them in case the preferences
* are set to show notifications.
*
* @author bpasero
*/
public class NotificationService {

  /* Batch News-Events for every 5 seconds */
  private static final int BATCH_INTERVAL = 5000;

  private final NewsListener fNewsListener;
  private final SearchMarkListener fSearchMarkListener;
  private final BookMarkListener fBookMarkListener;
  private final FolderListener fFolderListener;
  private final IPreferenceScope fGlobalPreferences;
  private final BatchedBuffer<NotificationItem> fBatchedBuffer;
  private final Map<String /* Feed Link */, Boolean /* Enablement State */> fNotifierEnablementCache = new ConcurrentHashMap<String, Boolean>();

  /* Singleton instance */
  private static NotificationPopup fgNotificationPopup;

  /* Remember Last Closing Time */
  private static long fgLastNotificationPopupCloseTime;

  /** Supported Modes of the Notifier */
  public enum Mode {

    /** Automatic Notification of Incoming News (default) */
    INCOMING_AUTOMATIC,

    /** Manual Notification of Incoming News */
    INCOMING_MANUAL,

    /** Notification of Recent News */
    RECENT
  }

  /** Creates a new Notification Service */
  public NotificationService() {

    /* Process Events batched */
    BatchedBuffer.Receiver<NotificationItem> receiver = new BatchedBuffer.Receiver<NotificationItem>() {
      public IStatus receive(Collection<NotificationItem> items, Job job, IProgressMonitor monitor) {
        if (!monitor.isCanceled())
          showItems(items, Mode.INCOMING_AUTOMATIC, monitor);

        return Status.OK_STATUS;
      }
    };

    fBatchedBuffer = new BatchedBuffer<NotificationItem>(receiver, BATCH_INTERVAL);
    fGlobalPreferences = Owl.getPreferenceService().getGlobalScope();
    fNewsListener = registerNewsListener();
    fSearchMarkListener = registerSearchMarkListener();
    fBookMarkListener = registerBookMarkListener();
    fFolderListener = registerFolderListener();
  }

  /** Shutdown this Service */
  public void stopService() {
    fBatchedBuffer.cancel(false);
    DynamicDAO.removeEntityListener(INews.class, fNewsListener);
    DynamicDAO.removeEntityListener(ISearchMark.class, fSearchMarkListener);
    DynamicDAO.removeEntityListener(IBookMark.class, fBookMarkListener);
    DynamicDAO.removeEntityListener(IFolder.class, fFolderListener);
  }

  /**
   * Called whenever settings on the {@link NotifierPreferencesPage} have
   * changed. In our case, we will invalidate the cache of enabled bookmarks.
   */
  public void notifySettingsChanged() {
    synchronized (fNotifierEnablementCache) {
      fNotifierEnablementCache.clear();
    }
  }

  /**
   * @param news the list of {@link INews} to show in the notifier.
   * @param color the color to use for the news or <code>null</code> if none.
   * @param mode the {@link Mode} of the notification
   */
  public void show(List<INews> news, RGB color, Mode mode) {
    show(news, color, mode, false);
  }

  /**
   * @param news the list of {@link INews} to show in the notifier.
   * @param color the color to use for the news or <code>null</code> if none.
   * @param mode the {@link Mode} of the notification
   * @param direct if <code>true</code> directly show the news in the notifier
   * without going through the delayed buffer, <code>false</code> otherwise.
   */
  public void show(List<INews> news, RGB color, Mode mode, boolean direct) {

    /* Create Notification Items */
    Set<NotificationItem> items = new TreeSet<NotificationItem>();
    for (INews newsitem : news)
      items.add(new NewsNotificationItem(newsitem, color));

    /* Add into Buffer for automatic */
    if (!direct && !isPopupVisible() && mode == Mode.INCOMING_AUTOMATIC)
      fBatchedBuffer.addAll(items);

    /* Show Directly otherwise */
    else
      showItems(items, mode, null);
  }

  /**
   * @return <code>true</code> if the popup notification is currently visible
   * and <code>false</code> otherwise.
   */
  public synchronized boolean isPopupVisible() {
    return fgNotificationPopup != null;
  }

  /**
   * @return <code>true</code> if the notification popup was recently closed and
   * <code>false</code> otherwise.
   */
  public boolean wasPopupRecentlyClosed() {
    return System.currentTimeMillis() - fgLastNotificationPopupCloseTime < 300;
  }

  /**
   * Close the notification popup if it is currently showing.
   */
  public synchronized void closePopup() {
    NotificationPopup popup = fgNotificationPopup;
    if (popup != null)
      popup.doClose();
  }

  /* Listen on News Events */
  private NewsListener registerNewsListener() {
    NewsListener listener = new NewsAdapter() {
      @Override
      public void entitiesAdded(final Set<NewsEvent> events) {
        if (!Controller.getDefault().isShuttingDown())
          onNewsAdded(events);
      }
    };

    DynamicDAO.addEntityListener(INews.class, listener);
    return listener;
  }

  private BookMarkListener registerBookMarkListener() {
    BookMarkListener listener = new BookMarkAdapter() {
      @Override
      public void entitiesUpdated(Set<BookMarkEvent> events) {
        for (BookMarkEvent event : events) {
          if (event.isRoot() && event.getOldParent() != null) {
            notifySettingsChanged();
            break;
          }
        }
      }
    };
    DynamicDAO.addEntityListener(IBookMark.class, listener);

    return listener;
  }

  private FolderListener registerFolderListener() {
    FolderListener listener = new FolderAdapter() {
      @Override
      public void entitiesUpdated(Set<FolderEvent> events) {
        for (FolderEvent event : events) {
          if (event.isRoot() && event.getOldParent() != null) {
            notifySettingsChanged();
            break;
          }
        }
      }
    };
    DynamicDAO.addEntityListener(IFolder.class, listener);

    return listener;
  }

  /* Listen on Search Mark Events */
  private SearchMarkListener registerSearchMarkListener() {
    SearchMarkListener listener = new SearchMarkAdapter() {
      @Override
      public void newsChanged(Set<SearchMarkEvent> events) {
        if (!Controller.getDefault().isShuttingDown())
          onNewsChanged(events);
      }
    };

    DynamicDAO.addEntityListener(ISearchMark.class, listener);
    return listener;
  }

  private void onNewsChanged(Set<SearchMarkEvent> events) {

    /* Return if Notification is disabled */
    if (!fGlobalPreferences.getBoolean(DefaultPreferences.SHOW_NOTIFICATION_POPUP))
      return;

    /* Filter Events if user decided to show Notifier only for selected Elements */
    Set<SearchMarkEvent> filteredEvents = new HashSet<SearchMarkEvent>(events.size());
    if (fGlobalPreferences.getBoolean(DefaultPreferences.LIMIT_NOTIFIER_TO_SELECTION)) {
      for (SearchMarkEvent event : events) {

        /* Check for new *new* News matching now */
        if (!event.isAddedNewNews())
          continue;

        /* Check for explicit selection */
        IPreferenceScope prefs = Owl.getPreferenceService().getEntityScope(event.getEntity());
        if (prefs.getBoolean(DefaultPreferences.ENABLE_NOTIFIER))
          filteredEvents.add(event);
      }
    }

    /* Filter Events based on other criterias otherwise */
    else {
      for (SearchMarkEvent event : events) {
        ISearchMark searchmark = event.getEntity();
        List<ISearchCondition> conditions = searchmark.getSearchConditions();

        /* Check for new *new* News matching now */
        if (!event.isAddedNewNews())
          continue;

        /* Look for a String search condition that is not Label */
        for (ISearchCondition condition : conditions) {
          if (condition.getValue() instanceof String && condition.getField().getId() != INews.LABEL) {
            filteredEvents.add(event);
            break;
          }
        }
      }
    }

    /* Create Items */
    Set<NotificationItem> items = new TreeSet<NotificationItem>();
    for (SearchMarkEvent event : filteredEvents)
      items.add(new SearchNotificationItem(event.getEntity(), event.getEntity().getNewsCount(EnumSet.of(INews.State.NEW, INews.State.UNREAD, INews.State.UPDATED))));

    /* Add into Buffer */
    if (!isPopupVisible())
      fBatchedBuffer.addAll(items);

    /* Show Directly */
    else
      showItems(items, Mode.INCOMING_AUTOMATIC, null);
  }

  private void onNewsAdded(final Set<NewsEvent> events) {

    /* Return if Notification is disabled */
    if (!fGlobalPreferences.getBoolean(DefaultPreferences.SHOW_NOTIFICATION_POPUP))
      return;

    /* Return if events are not containing any NEW News */
    if (!CoreUtils.containsState(events, INews.State.NEW))
      return;

    /* Use a runnable for this piece of code as it might be executed from an async call or not */
    final Runnable runnable = new Runnable() {
      public void run() {
        Set<NewsEvent> eventsToShow = events;

        /* Filter Events if user decided to show Notifier only for selected Elements */
        if (fGlobalPreferences.getBoolean(DefaultPreferences.LIMIT_NOTIFIER_TO_SELECTION))
          eventsToShow = filterEvents(eventsToShow);

        /* Create Items */
        Set<NotificationItem> items = new TreeSet<NotificationItem>();
        for (NewsEvent event : eventsToShow) {
          INews news = event.getEntity();
          if (news.getState().equals(INews.State.NEW)) //Only show NEW news in Notifier
            items.add(new NewsNotificationItem(news));
        }

        /* Return if nothing to show */
        if (items.isEmpty())
          return;

        /* Add into Buffer */
        if (!isPopupVisible())
          fBatchedBuffer.addAll(items);

        /* Show Directly */
        else
          showItems(items, Mode.INCOMING_AUTOMATIC, null);
      }
    };

    /*
     * Optimization and Workaround for a Bug: It is quite useless to send notification items
     * to the buffer for showing in the notifier if the user has configured RSSOwl to only show
     * a notification when the application window is minimized and the window is currently not
     * minimized. This also fixes a timing issue where quickly minimizing the window after
     * receving news items would show them in the notifier (although they might have been read
     * before already). The fix is to check in the UI thread whether the application window
     * is minimized or not.
     */
    if (fGlobalPreferences.getBoolean(DefaultPreferences.SHOW_NOTIFICATION_POPUP_ONLY_WHEN_MINIMIZED)) {
      Shell primaryShell = OwlUI.getPrimaryShell();
      if (primaryShell != null) {
        JobRunner.runInUIThread(primaryShell, new Runnable() { //MUST NOT RUN SYNCED IN UI THREAD FROM EVENT - DEADLOCK ALERT !!!
          public void run() {
            if (Controller.getDefault().isShuttingDown())
              return;

            ApplicationWorkbenchWindowAdvisor advisor = ApplicationWorkbenchAdvisor.fgPrimaryApplicationWorkbenchWindowAdvisor;
            if (advisor != null && !advisor.isMinimizedToTray() && !advisor.isMinimized())
              return;

            JobRunner.runInBackgroundThread(runnable);
          }
        });
      }
    }

    /* Directly Execute */
    else
      runnable.run();
  }

  /**
   * @param news the {@link INews} to check if it can show or not.
   * @return <code>true</code> if the news can be shown and <code>false</code>
   * if it should not show due to filter rules.
   */
  public boolean shouldShow(INews news) {
    if (!fGlobalPreferences.getBoolean(DefaultPreferences.LIMIT_NOTIFIER_TO_SELECTION))
      return true;

    return shouldShow(news.getFeedLinkAsText());
  }

  private boolean shouldShow(String feedLink) {
    synchronized (fNotifierEnablementCache) {
      if (!fNotifierEnablementCache.containsKey(feedLink))
        updateEnabledFeedCache();

      Boolean notifierEnabled = fNotifierEnablementCache.get(feedLink);
      if (notifierEnabled == null) {
        notifierEnabled = false;
        fNotifierEnablementCache.put(feedLink, notifierEnabled);
      }

      return notifierEnabled;
    }
  }

  private Set<NewsEvent> filterEvents(Set<NewsEvent> events) {
    Set<NewsEvent> filteredEvents = new HashSet<NewsEvent>();

    for (NewsEvent event : events) {
      if (!event.getEntity().isVisible())
        continue;

      if (shouldShow(event.getEntity().getFeedLinkAsText()))
        filteredEvents.add(event);
    }

    return filteredEvents;
  }

  private void updateEnabledFeedCache() {
    Collection<IBookMark> bookMarks = DynamicDAO.loadAll(IBookMark.class);
    for (IBookMark bookMark : bookMarks) {
      IPreferenceScope prefs = Owl.getPreferenceService().getEntityScope(bookMark);
      fNotifierEnablementCache.put(bookMark.getFeedLinkReference().getLinkAsText(), prefs.getBoolean(DefaultPreferences.ENABLE_NOTIFIER));
    }
  }

  /* Show Notification in UI Thread */
  private void showItems(final Collection<NotificationItem> items, final Mode mode, final IProgressMonitor monitor) {

    /* Ignore empty lists */
    if (items.isEmpty())
      return;

    /* Make sure to run in UI Thread */
    JobRunner.runInUIThread(OwlUI.getPrimaryShell(), new Runnable() {
      public void run() {

        /* Return early if shutting down */
        if (Controller.getDefault().isShuttingDown() || (monitor != null && monitor.isCanceled()))
          return;

        /* Return if Notification should only show when minimized */
        ApplicationWorkbenchWindowAdvisor advisor = ApplicationWorkbenchAdvisor.fgPrimaryApplicationWorkbenchWindowAdvisor;
        boolean minimized = advisor != null && (advisor.isMinimizedToTray() || advisor.isMinimized());
        if (!minimized && fGlobalPreferences.getBoolean(DefaultPreferences.SHOW_NOTIFICATION_POPUP_ONLY_WHEN_MINIMIZED))
          return;

        /* Show News in Popup */
        synchronized (NotificationService.this) {

          /* Popup not yet visible, create new */
          if (fgNotificationPopup == null) {
            fgNotificationPopup = new NotificationPopup(items.size(), mode) {
              @Override
              public boolean doClose() {
                fgNotificationPopup = null;
                fgLastNotificationPopupCloseTime = System.currentTimeMillis();
                return super.doClose();
              }
            };

            try {
              fgNotificationPopup.open(items);
            }

            /*
             * For some reason a NPE is raised from Decorations.restoreFocus(Decorations.java:806)
             * as outlined in Bug 1389 (NullPointer while clicking on tray icon). This is actually
             * caused by a bug in Eclipse (https://bugs.eclipse.org/bugs/show_bug.cgi?id=212219)
             * that is only fixed in version 3.5 and newer.
             */
            catch (Exception e) {
              fgNotificationPopup = null;
            }
          }

          /* Notifier already opened - Show Items (only for automatic) */
          else if (mode == Mode.INCOMING_AUTOMATIC)
            fgNotificationPopup.makeVisible(items);
        }
      }
    });
  }
}
TOP

Related Classes of org.rssowl.ui.internal.notifier.NotificationService

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.