/*
* This file is part of muCommander, http://www.mucommander.com
* Copyright (C) 2002-2016 Maxence Bernard
*
* muCommander 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 3 of the License, or
* (at your option) any later version.
*
* muCommander 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, see <http://www.gnu.org/licenses/>.
*/
package com.mucommander.core;
import java.awt.event.WindowEvent;
import java.awt.event.WindowListener;
import java.util.List;
import java.util.Vector;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.mucommander.commons.file.AbstractFile;
import com.mucommander.commons.file.filter.AbstractFileFilter;
import com.mucommander.commons.file.filter.FileFilter;
import com.mucommander.commons.file.filter.OrFileFilter;
import com.mucommander.commons.file.protocol.FileProtocols;
import com.mucommander.conf.MuConfigurations;
import com.mucommander.conf.MuPreference;
import com.mucommander.conf.MuPreferences;
import com.mucommander.job.JobsManager;
import com.mucommander.ui.event.LocationEvent;
import com.mucommander.ui.event.LocationListener;
import com.mucommander.ui.main.FolderPanel;
/**
* This file monitors changes in the current folder of a FolderPanel, checking periodically if the current folder's
* date has changed. If a change has been detected, the FolderPanel will be asked to refresh its current folder.
*
* <p>If the MainFrame which contains the monitored FolderPanel becomes inactive (lies in the background), monitoring
* on will be not happen until the MainFrame becomes active again.
*
* <p>Implementation note: the monitoring is done in one single thread for all folders, each folder being monitored
* one after another. Current folder refreshes are performed in a separate thread.
*
* @author Maxence Bernard
* @see <a href="http://trac.mucommander.com/wiki/FolderAutoRefresh">FolderAutoRefresh wiki entry</a>
*/
public class FolderChangeMonitor implements Runnable, WindowListener, LocationListener {
private static final Logger LOGGER = LoggerFactory.getLogger(FolderChangeMonitor.class);
/** Folder panel we are monitoring */
private FolderPanel folderPanel;
/** Current file table's folder */
private AbstractFile currentFolder;
/** True when the current folder is currently being changed */
private boolean folderChanging;
/** Current folder's date */
private long currentFolderDate;
/** Number of milliseconds to wait before next folder check */
private long waitBeforeCheckTime;
/** Timestamp of the last folder change check */
private long lastCheckTimestamp;
/** Total time spent checking for folder changes in current folder */
private long totalCheckTime = 0;
/** Number of checks in current folder */
private int nbSamples = 0;
//////////////////////
// Static variables //
//////////////////////
/** Thread in which the actual monitoring is performed */
private static Thread monitorThread;
/** FolderChangeMonitor instances */
private static List<FolderChangeMonitor> instances;
private static OrFileFilter disableAutoRefreshFilter = new OrFileFilter();
/** Milliseconds period between checks to current folder's date */
private static long checkPeriod;
/** Delay in milliseconds before folder date check after a folder has been refreshed */
private static long waitAfterRefresh;
/** If folder change check took an average of N milliseconds, thread will wait at least N*WAIT_MULTIPLIER before next check */
private final static int WAIT_MULTIPLIER = 50;
/** Granularity of the thread check (number of milliseconds to sleep before next loop) */
private final static int TICK = 300;
static {
instances = new Vector<FolderChangeMonitor>();
// Retrieve configuration values
checkPeriod = MuConfigurations.getPreferences().getVariable(MuPreference.REFRESH_CHECK_PERIOD,
MuPreferences.DEFAULT_REFRESH_CHECK_PERIOD);
waitAfterRefresh = MuConfigurations.getPreferences().getVariable(MuPreference.WAIT_AFTER_REFRESH,
MuPreferences.DEFAULT_WAIT_AFTER_REFRESH);
disableAutoRefreshFilter.addFileFilter(new AbstractFileFilter() {
public boolean accept(AbstractFile file) {
return file.getURL().getScheme().equals(FileProtocols.S3);
}
});
}
/**
* Adds the given {@link FileFilter} to the list of filters that match folders for which auto-refresh is disabled.
* One use case for disabling auto-refresh is for protocols that involve a cost ($$$) when looking for changes
* or refreshing the folder. This is the case for Amazon S3 for which auto-refresh is disabled by default.
*
* @param filter matches folders for which auto-refresh will be disabled
*/
public static void addDisableAutoRefreshFilter(FileFilter filter) {
disableAutoRefreshFilter.addFileFilter(filter);
}
public FolderChangeMonitor(FolderPanel folderPanel) {
this.folderPanel = folderPanel;
// Listen to folder changes to know when a folder is being / has been changed
folderPanel.getLocationManager().addLocationListener(this);
this.currentFolder = folderPanel.getCurrentFolder();
this.currentFolderDate = currentFolder.getDate();
// Folder contents is up-to-date let's wait before checking it for changes
this.lastCheckTimestamp = System.currentTimeMillis();
this.waitBeforeCheckTime = waitAfterRefresh;
folderPanel.getMainFrame().addWindowListener(this);
instances.add(this);
// Create and start the monitor thread on first FolderChangeMonitor instance
if(monitorThread==null && checkPeriod>=0) {
monitorThread = new Thread(this, getClass().getName());
monitorThread.setDaemon(true);
monitorThread.start();
}
}
public void run() {
// TODO: it would be more efficient to use a wait/notify scheme rather than sleeping.
// It would also allow folders to be checked immediately upon certain conditions such as a window becoming activated.
while (monitorThread!=null) {
// Sleep for a while
try { Thread.sleep(TICK);}
catch(InterruptedException e) {}
// Loop on instances
int nbInstances = instances.size();
for (int i=0; i<nbInstances; i++) {
FolderChangeMonitor monitor;
try { monitor = instances.get(i); }
catch(Exception e) { continue; } // Exception may be raised when an instance is removed
// Check for changes in current folder and refresh it only if :
// - MainFrame is in the foreground
// - current folder is not being changed
if (monitor.folderPanel.getMainFrame().isForegroundActive() && !monitor.folderChanging) {
if (disableAutoRefreshFilter.match(monitor.currentFolder)) {
monitor.lastCheckTimestamp = System.currentTimeMillis();
monitor.waitBeforeCheckTime = checkPeriod;
continue;
}
// By checking FolderPanel.getLastFolderChangeTime(), we ensure that we don't check right after
// the folder has been refreshed.
if (System.currentTimeMillis()-Math.max(monitor.lastCheckTimestamp, monitor.folderPanel.getLastFolderChangeTime())>monitor.waitBeforeCheckTime) {
// Checks folder contents and refreshes view if necessary
monitor.waitBeforeCheckTime = monitor.checkAndRefresh();
monitor.lastCheckTimestamp = System.currentTimeMillis();
}
}
}
}
}
/**
* Stops monitoring (stops monitoring thread).
*/
public void stop() {
monitorThread = null;
}
/**
* Forces this monitor to update current folder information. This method should be called when a folder has been
* manually refreshed, so that this monitor doesn't detect changes and try to refresh the table again.
*
* @param folder the new current folder
*/
private void updateFolderInfo(AbstractFile folder) {
this.currentFolder = folder;
this.currentFolderDate = currentFolder.getDate();
// Reset time average
totalCheckTime = 0;
nbSamples = 0;
}
/**
* Refresh the file table if running file jobs could not change the current file table's folder and if current
* folder's date has changed.
*
* @return the time (msec) to wait before next refresh attempt
* Note that folder change check took an average of N milliseconds, the returned value will be at least N*WAIT_MULTIPLIER
*/
private synchronized long checkAndRefresh() {
if (!mayFolderChangeByFileJob() && isFolderDateChanged()) {
// Try and refresh current folder in a separate thread as to not lock monitor thread
folderPanel.tryRefreshCurrentFolder();
return nbSamples==0 ?
waitAfterRefresh
: Math.max(waitAfterRefresh, (int)(WAIT_MULTIPLIER*(totalCheckTime/(float)nbSamples)));
}
return nbSamples==0 ?
checkPeriod
: Math.max(checkPeriod, (int)(WAIT_MULTIPLIER*(totalCheckTime/(float)nbSamples)));
}
private boolean mayFolderChangeByFileJob() {
return JobsManager.getInstance().mayFolderChangeByExistingJob(currentFolder);
}
/**
* Has date changed ?
* Note that date will be 0 if the folder is no longer available, and thus yield a refresh: this is exactly
* what we want (the folder will be changed to a 'workable' folder).
*/
private boolean isFolderDateChanged() {
// Update time average next loop
long timeStamp = System.currentTimeMillis();
// Check folder's date
long date = currentFolder.getDate();
totalCheckTime += System.currentTimeMillis()-timeStamp;
nbSamples++;
if (date == currentFolderDate)
return false;
LOGGER.debug(this+" ("+currentFolder.getName()+") Detected changes in current folder, refreshing table!");
return true;
}
/////////////////////////////////////
// LocationListener implementation //
/////////////////////////////////////
public void locationChanging(LocationEvent locationEvent) {
folderChanging = true;
}
public void locationChanged(LocationEvent locationEvent) {
// Update new current folder info
updateFolderInfo(locationEvent.getFolderPanel().getCurrentFolder());
folderChanging = false;
}
public void locationCancelled(LocationEvent locationEvent) {
folderChanging = false;
}
public void locationFailed(LocationEvent locationEvent) {
folderChanging = false;
}
///////////////////////////////////
// WindowListener implementation //
///////////////////////////////////
public void windowActivated(WindowEvent e) {}
public void windowDeactivated(WindowEvent e) {}
public void windowIconified(WindowEvent e) {}
public void windowDeiconified(WindowEvent e) {}
public void windowOpened(WindowEvent e) {}
public void windowClosing(WindowEvent e) {}
public void windowClosed(WindowEvent e) {
// Remove the MainFrame from the list of monitored instances
instances.remove(this);
LOGGER.debug("nbInstances="+instances.size());
}
}