org.jajuk.base.Device.java Source code

Java tutorial

Introduction

Here is the source code for org.jajuk.base.Device.java

Source

/*
 *  Jajuk
 *  Copyright (C) The Jajuk Team
 *  http://jajuk.info
 *
 *  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 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.
 *  
 */
package org.jajuk.base;

import java.io.File;
import java.io.FileFilter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import javax.swing.ImageIcon;
import javax.swing.JOptionPane;
import javax.swing.SwingUtilities;

import org.apache.commons.lang.StringUtils;
import org.jajuk.events.JajukEvent;
import org.jajuk.events.JajukEvents;
import org.jajuk.events.ObservationManager;
import org.jajuk.services.bookmark.History;
import org.jajuk.services.core.ExitService;
import org.jajuk.services.players.QueueModel;
import org.jajuk.ui.helpers.ManualDeviceRefreshReporter;
import org.jajuk.ui.helpers.RefreshReporter;
import org.jajuk.ui.widgets.InformationJPanel;
import org.jajuk.ui.windows.JajukMainWindow;
import org.jajuk.util.Conf;
import org.jajuk.util.Const;
import org.jajuk.util.IconLoader;
import org.jajuk.util.JajukFileFilter;
import org.jajuk.util.JajukIcons;
import org.jajuk.util.Messages;
import org.jajuk.util.UtilGUI;
import org.jajuk.util.UtilString;
import org.jajuk.util.UtilSystem;
import org.jajuk.util.error.JajukException;
import org.jajuk.util.filters.ImageFilter;
import org.jajuk.util.filters.KnownTypeFilter;
import org.jajuk.util.log.Log;
import org.xml.sax.Attributes;

/**
 * A device ( music files repository )
 * <p>
 * Some properties of a device are immutable : name, url and type *
 * <p>
 * Physical item.
 */
public class Device extends PhysicalItem implements Comparable<Device> {
    /** The Constant OPTION_REFRESH_DEEP.*/
    private static final int OPTION_REFRESH_DEEP = 1;
    /** The Constant OPTION_REFRESH_CANCEL. */
    private static final int OPTION_REFRESH_CANCEL = 2;

    // Device type constants
    /**
     * .
     */
    public enum Type {
        DIRECTORY, FILES_CD, NETWORK_DRIVE, EXTDD, PLAYER
    }

    /** Device URL (performances). */
    private String sUrl;
    /** IO file for optimizations*. */
    private java.io.File fio;
    /** Mounted device flag. */
    private boolean bMounted = false;
    /** directories. */
    private final List<Directory> alDirectories = new ArrayList<Directory>(20);
    /** Already refreshing flag. */
    private volatile boolean bAlreadyRefreshing = false; //NOSONAR
    /** Already synchronizing flag. */
    private volatile boolean bAlreadySynchronizing = false; //NOSONAR
    /** Volume of created files during synchronization. */
    private long lVolume = 0;
    /** date last refresh. */
    private long lDateLastRefresh;
    /** Progress reporter *. */
    private RefreshReporter reporter;
    /** Refresh deepness choice *. */
    private int choice = Device.OPTION_REFRESH_DEEP;
    /** [PERF] cache rootDir directory. */
    private Directory rootDir;

    /**
     * Device constructor.
     * 
     * @param sId 
     * @param sName 
     */
    Device(final String sId, final String sName) {
        super(sId, sName);
    }

    /**
     * Adds the directory.
     * 
     * @param directory 
     */
    void addDirectory(final Directory directory) {
        alDirectories.add(directory);
    }

    /**
     * Scan directories to cleanup removed files and playlists.
     * 
     * @param dirsToRefresh list of the directory to refresh, null if all of them
     * 
     * @return whether some items have been removed
     */
    public boolean cleanRemovedFiles(List<Directory> dirsToRefresh) {
        long l = System.currentTimeMillis();
        // directories cleanup
        boolean bChanges = cleanDirectories(dirsToRefresh);
        // files cleanup
        bChanges = bChanges | cleanFiles(dirsToRefresh);
        // Playlist cleanup
        bChanges = bChanges | cleanPlaylist(dirsToRefresh);
        // clear history to remove old files referenced in it
        if (Conf.getString(Const.CONF_HISTORY) != null) {
            History.getInstance().clear(Integer.parseInt(Conf.getString(Const.CONF_HISTORY)));
        }
        // delete old history items
        l = System.currentTimeMillis() - l;
        Log.debug("{{" + getName() + "}} Old file references cleaned in: "
                + ((l < 1000) ? l + " ms, changes: " + bChanges : l / 1000 + " s, changes: " + bChanges));
        return bChanges;
    }

    /**
     * Walk through all Playlists and remove the ones for the current device.
     * 
     * @param dirsToRefresh list of the directory to refresh, null if all of them
     * 
     * @return true if there was any playlist removed
     */
    private boolean cleanPlaylist(List<Directory> dirsToRefresh) {
        boolean bChanges = false;
        final List<Playlist> plfiles = PlaylistManager.getInstance().getPlaylists();
        for (final Playlist plf : plfiles) {
            // check if it is a playlist located inside refreshed directory
            if (dirsToRefresh != null) {
                boolean checkIt = false;
                for (Directory directory : dirsToRefresh) {
                    if (plf.hasAncestor(directory)) {
                        checkIt = true;
                    }
                }
                // This item is not in given directories, just continue
                if (!checkIt) {
                    continue;
                }
            }
            if (!ExitService.isExiting() && plf.getDirectory().getDevice().equals(this) && plf.isReady()
                    && !plf.getFIO().exists()) {
                PlaylistManager.getInstance().removeItem(plf);
                Log.debug("Removed: " + plf);
                if (reporter != null) {
                    reporter.notifyFileOrPlaylistDropped();
                }
                bChanges = true;
            }
        }
        return bChanges;
    }

    /**
     * Walk through tall Files and remove the ones for the current device.
     * 
     * @param dirsToRefresh list of the directory to refresh, null if all of them
     * 
     * @return true if there was any file removed.
     */
    private boolean cleanFiles(List<Directory> dirsToRefresh) {
        boolean bChanges = false;
        final List<org.jajuk.base.File> files = FileManager.getInstance().getFiles();
        for (final org.jajuk.base.File file : files) {
            // check if it is a file located inside refreshed directory
            if (dirsToRefresh != null) {
                boolean checkIt = false;
                for (Directory directory : dirsToRefresh) {
                    if (file.hasAncestor(directory)) {
                        checkIt = true;
                    }
                }
                // This item is not in given directories, just continue
                if (!checkIt) {
                    continue;
                }
            }
            if (!ExitService.isExiting() && file.getDirectory().getDevice().equals(this) && file.isReady() &&
            // Remove file if it doesn't exist any more or if it is a iTunes
            // file (useful for jajuk < 1.4)
                    (!file.getFIO().exists() || file.getName().startsWith("._"))) {
                FileManager.getInstance().removeFile(file);
                Log.debug("Removed: " + file);
                bChanges = true;
                if (reporter != null) {
                    reporter.notifyFileOrPlaylistDropped();
                }
            }
        }
        return bChanges;
    }

    /**
     * Walks through all directories and removes the ones for this device.
     * 
     * @param dirsToRefresh list of the directory to refresh, null if all of them
     * 
     * @return true if there was any directory removed
     */
    private boolean cleanDirectories(List<Directory> dirsToRefresh) {
        boolean bChanges = false;
        List<Directory> dirs = null;
        if (dirsToRefresh == null) {
            dirs = DirectoryManager.getInstance().getDirectories();
        } else {
            // If one or more named directories are provided, not only clean them up but also their sub directories
            dirs = new ArrayList<Directory>(dirsToRefresh);
            for (Directory dir : dirsToRefresh) {
                dirs.addAll(dir.getDirectoriesRecursively());
            }
        }
        for (final Directory dir : dirs) {
            if (!ExitService.isExiting() && dir.getDevice().equals(this) && dir.getDevice().isMounted()
                    && !dir.getFio().exists()) {
                // note that associated files are removed too
                DirectoryManager.getInstance().removeDirectory(dir.getID());
                Log.debug("Removed: " + dir);
                bChanges = true;
            }
        }
        return bChanges;
    }

    /**
     * Alphabetical comparator used to display ordered lists of devices.
     * 
     * @param otherDevice 
     * 
     * @return comparison result
     */
    @Override
    public int compareTo(final Device otherDevice) {
        // should handle null
        if (otherDevice == null) {
            return -1;
        }
        // We must be consistent with equals, see
        // http://java.sun.com/javase/6/docs/api/java/lang/Comparable.html
        int comp = getName().compareToIgnoreCase(otherDevice.getName());
        if (comp == 0) {
            return getName().compareTo(otherDevice.getName());
        } else {
            return comp;
        }
    }

    /**
     * Gets the date last refresh.
     * 
     * @return the date last refresh
     */
    public long getDateLastRefresh() {
        return lDateLastRefresh;
    }

    /* (non-Javadoc)
     * @see org.jajuk.base.Item#getTitle()
     */
    @Override
    public String getTitle() {
        return Messages.getString("Item_Device") + " : " + getName();
    }

    /**
     * Gets the device type as a string.
     * 
     * @return the device type as string
     */
    public String getDeviceTypeS() {
        return getType().name();
    }

    /**
     * Gets the directories directly under the device root (not recursive).
     * 
     * @return the directories
     */
    public List<Directory> getDirectories() {
        return alDirectories;
    }

    /**
     * return ordered child files recursively.
     * 
     * @return child files recursively
     */
    public List<org.jajuk.base.File> getFilesRecursively() {
        // looks for the root directory for this device
        Directory dirRoot = getRootDirectory();
        if (dirRoot != null) {
            return dirRoot.getFilesRecursively();
        }
        // nothing found, return empty list
        return new ArrayList<org.jajuk.base.File>();
    }

    /**
     * Gets the fio.
     * 
     * @return Returns the IO file reference to this directory.
     */
    public File getFIO() {
        return fio;
    }

    /*
     * (non-Javadoc)
     * 
     * @see org.jajuk.base.Item#getHumanValue(java.lang.String)
     */
    @Override
    public String getHumanValue(final String sKey) {
        if (Const.XML_TYPE.equals(sKey)) {
            return getTypeLabel(getType());
        } else {// default
            return super.getHumanValue(sKey);
        }
    }

    /**
     * Return label for a type.
     *
     * @param type 
     * @return label for a type
     */
    public static String getTypeLabel(Type type) {
        if (type == Type.DIRECTORY) {
            return Messages.getString("Device_type.directory");
        } else if (type == Type.FILES_CD) {
            return Messages.getString("Device_type.file_cd");
        } else if (type == Type.EXTDD) {
            return Messages.getString("Device_type.extdd");
        } else if (type == Type.PLAYER) {
            return Messages.getString("Device_type.player");
        } else if (type == Type.NETWORK_DRIVE) {
            return Messages.getString("Device_type.network_drive");
        } else {
            return null;
        }
    }

    /*
     * (non-Javadoc)
     * 
     * @see org.jajuk.base.Item#getIconRepresentation()
     */
    @Override
    public ImageIcon getIconRepresentation() {
        if (getType() == Type.DIRECTORY) {
            return rightIcon(IconLoader.getIcon(JajukIcons.DEVICE_DIRECTORY_MOUNTED_SMALL),
                    IconLoader.getIcon(JajukIcons.DEVICE_DIRECTORY_UNMOUNTED_SMALL));
        } else if (getType() == Type.FILES_CD) {
            return rightIcon(IconLoader.getIcon(JajukIcons.DEVICE_CD_MOUNTED_SMALL),
                    IconLoader.getIcon(JajukIcons.DEVICE_CD_UNMOUNTED_SMALL));
        } else if (getType() == Type.NETWORK_DRIVE) {
            return rightIcon(IconLoader.getIcon(JajukIcons.DEVICE_NETWORK_DRIVE_MOUNTED_SMALL),
                    IconLoader.getIcon(JajukIcons.DEVICE_NETWORK_DRIVE_UNMOUNTED_SMALL));
        } else if (getType() == Type.EXTDD) {
            return rightIcon(IconLoader.getIcon(JajukIcons.DEVICE_EXT_DD_MOUNTED_SMALL),
                    IconLoader.getIcon(JajukIcons.DEVICE_EXT_DD_UNMOUNTED_SMALL));
        } else if (getType() == Type.PLAYER) {
            return rightIcon(IconLoader.getIcon(JajukIcons.DEVICE_PLAYER_MOUNTED_SMALL),
                    IconLoader.getIcon(JajukIcons.DEVICE_PLAYER_UNMOUNTED_SMALL));
        } else {
            Log.warn("Unknown type of device detected: " + getType().name());
            return null;
        }
    }

    /*
     * Return large icon representation of the device
     * @Return large icon representation of the device
     */
    /**
     * Gets the icon representation large.
     *
     * @return the icon representation large
     */
    public ImageIcon getIconRepresentationLarge() {
        if (getType() == Type.DIRECTORY) {
            return rightIcon(IconLoader.getIcon(JajukIcons.DEVICE_DIRECTORY_MOUNTED),
                    IconLoader.getIcon(JajukIcons.DEVICE_DIRECTORY_UNMOUNTED));
        } else if (getType() == Type.FILES_CD) {
            return rightIcon(IconLoader.getIcon(JajukIcons.DEVICE_CD_MOUNTED),
                    IconLoader.getIcon(JajukIcons.DEVICE_CD_UNMOUNTED));
        } else if (getType() == Type.NETWORK_DRIVE) {
            return rightIcon(IconLoader.getIcon(JajukIcons.DEVICE_NETWORK_DRIVE_MOUNTED),
                    IconLoader.getIcon(JajukIcons.DEVICE_NETWORK_DRIVE_UNMOUNTED));
        } else if (getType() == Type.EXTDD) {
            return rightIcon(IconLoader.getIcon(JajukIcons.DEVICE_EXT_DD_MOUNTED),
                    IconLoader.getIcon(JajukIcons.DEVICE_EXT_DD_UNMOUNTED));
        } else if (getType() == Type.PLAYER) {
            return rightIcon(IconLoader.getIcon(JajukIcons.DEVICE_PLAYER_MOUNTED),
                    IconLoader.getIcon(JajukIcons.DEVICE_PLAYER_UNMOUNTED));
        } else {
            Log.warn("Unknown type of device detected: " + getType().name());
            return null;
        }
    }

    /**
     * Return the right icon between mounted or unmounted.
     *
     * @param mountedIcon The icon to return for a mounted device
     * @param unmountedIcon The icon to return for an unmounted device
     * @return Returns either of the two provided icons depending on the state of
     * the device
     */
    private ImageIcon rightIcon(ImageIcon mountedIcon, ImageIcon unmountedIcon) {
        if (isMounted()) {
            return mountedIcon;
        } else {
            return unmountedIcon;
        }
    }

    /*
     * (non-Javadoc)
     * 
     * @see org.jajuk.base.Item#getIdentifier()
     */
    @Override
    public final String getXMLTag() {
        return Const.XML_DEVICE;
    }

    /**
     * Gets the root directory.
     * 
     * @return Associated root directory
     */
    public Directory getRootDirectory() {
        if (rootDir == null) {
            rootDir = DirectoryManager.getInstance().getDirectoryForIO(getFIO(), this);
        }
        return rootDir;
    }

    /**
     * Gets the type.
     * 
     * @return the type
     */
    public Device.Type getType() {
        return Type.values()[(int) getLongValue(Const.XML_TYPE)];
    }

    /**
     * Gets the url.
     * 
     * @return the url
     */
    public String getUrl() {
        return sUrl;
    }

    /**
     * Checks if is mounted.
     * 
     * @return true, if is mounted
     */
    public boolean isMounted() {
        return bMounted;
    }

    /**
     * Return true if the device can be accessed right now.
     * 
     * @return true the file can be accessed right now
     */
    public boolean isReady() {
        if (isMounted() && !isRefreshing() && !isSynchronizing()) {
            return true;
        }
        return false;
    }

    /**
     * Tells if a device is refreshing.
     * 
     * @return true, if checks if is refreshing
     */
    public boolean isRefreshing() {
        return bAlreadyRefreshing;
    }

    /**
     * Tells if a device is synchronizing.
     * 
     * @return true, if checks if is synchronizing
     */
    public boolean isSynchronizing() {
        return bAlreadySynchronizing;
    }

    /**
     * Manual refresh, displays a dialog.
     * 
     * @param bAsk ask for refreshing type (deep or fast ?)
     * @param bAfterMove is this refresh done after a device location change ?
     * @param forcedDeep : override bAsk and force a deep refresh
     * @param dirsToRefresh : only refresh specified dirs, or all of them if null
     */
    public void manualRefresh(final boolean bAsk, final boolean bAfterMove, final boolean forcedDeep,
            List<Directory> dirsToRefresh) {
        int i = 0;
        try {
            i = prepareRefresh(bAsk);
            if (i == OPTION_REFRESH_CANCEL) {
                return;
            }
            bAlreadyRefreshing = true;
        } catch (JajukException je) {
            Messages.showErrorMessage(je.getCode());
            Log.debug(je);
            return;
        }
        try {
            reporter = new ManualDeviceRefreshReporter(this);
            reporter.startup();
            // clean old files up (takes a while)
            if (!bAfterMove) {
                cleanRemovedFiles(dirsToRefresh);
            }
            reporter.cleanupDone();
            // Actual refresh
            refreshCommand(((i == Device.OPTION_REFRESH_DEEP) || forcedDeep), true, dirsToRefresh);
            // cleanup logical items
            org.jajuk.base.Collection.cleanupLogical();
            // if it is a move, clean old files *after* the refresh
            if (bAfterMove) {
                cleanRemovedFiles(dirsToRefresh);
            }
            // notify views to refresh
            ObservationManager.notify(new JajukEvent(JajukEvents.DEVICE_REFRESH));
        } finally {
            // Do not let current reporter as a manual reporter because it would fail
            // in NPE with auto-refresh
            reporter = null;
            // Make sure to unlock refreshing
            bAlreadyRefreshing = false;
        }
    }

    /**
     * Prepare manual refresh.
     * 
     * @param bAsk ask user to perform deep or fast refresh 
     * 
     * @return the user choice (deep or fast)
     * 
     * @throws JajukException if user canceled, device cannot be refreshed or device already
     * refreshing
     */
    int prepareRefresh(final boolean bAsk) throws JajukException {
        if (bAsk) {
            final Object[] possibleValues = { Messages.getString("FilesTreeView.60"), // fast
                    Messages.getString("FilesTreeView.61"), // deep
                    Messages.getString("Cancel") };// cancel
            try {
                SwingUtilities.invokeAndWait(new Runnable() {
                    @Override
                    public void run() {
                        choice = JOptionPane.showOptionDialog(JajukMainWindow.getInstance(),
                                Messages.getString("FilesTreeView.59"), Messages.getString("Option"),
                                JOptionPane.DEFAULT_OPTION, JOptionPane.QUESTION_MESSAGE, null, possibleValues,
                                possibleValues[0]);
                    }
                });
            } catch (Exception e) {
                Log.error(e);
                choice = Device.OPTION_REFRESH_CANCEL;
            }
            if (choice == Device.OPTION_REFRESH_CANCEL) { // Cancel
                return choice;
            }
        }
        // JajukException are not trapped, will be thrown to the caller
        final Device device = this;
        if (!device.isMounted()) {
            // Leave if user canceled device mounting
            if (!device.mount(true)) {
                return Device.OPTION_REFRESH_CANCEL;
            }
        }
        if (bAlreadyRefreshing) {
            throw new JajukException(107);
        }
        return choice;
    }

    /**
     * Check that the device is available and not void.
     * <p>We Cannot mount void devices because of the jajuk reference cleanup thread 
     * ( a refresh would clear the entire device collection)</p>
     * 
     * @return true if the device is ready for mounting, false if the device is void
     * 
     */
    private boolean checkDevice() {
        return pathExists() && !isVoid();
    }

    /**
     * Return whether a device maps a void directory.
     *
     * @return whether a device maps a void directory
     */
    private boolean isVoid() {
        final File file = new File(getUrl());
        return (file.listFiles() == null || file.listFiles().length == 0);
    }

    /**
     * Return whether the device path exists at this time.
     *
     * @return whether the device path exists at this time
     */
    private boolean pathExists() {
        final File file = new File(getUrl());
        return file.exists();
    }

    /**
     * Mount the device.
     * 
     * @param bManual set whether mount is manual or auto
     * 
     * @return whether the device has been mounted. If user is asked for mounting but cancel, this method returns false.
     * 
     * @throws JajukException if device cannot be mounted due to technical reason.
     */
    public boolean mount(final boolean bManual) throws JajukException {
        if (bMounted) {
            // Device already mounted
            throw new JajukException(111);
        }
        // Check if we can mount the device. 
        boolean readyToMount = checkDevice();
        // Effective mounting if available.
        if (readyToMount) {
            bMounted = true;
        } else if (pathExists() && isVoid() && bManual) {
            // If the device is void and in manual mode, leave a chance to the user to 
            // force it
            final int answer = Messages.getChoice(
                    "[" + getName() + "] " + Messages.getString("Confirmation_void_refresh"),
                    JOptionPane.YES_NO_CANCEL_OPTION, JOptionPane.WARNING_MESSAGE);
            // leave if user doesn't confirm to mount the void device
            if (answer != JOptionPane.YES_OPTION) {
                return false;
            } else {
                bMounted = true;
            }
        } else {
            throw new JajukException(11, "\"" + getName() + "\" at URL : " + getUrl());
        }
        // notify views to refresh if needed
        ObservationManager.notify(new JajukEvent(JajukEvents.DEVICE_MOUNT));
        return bMounted;
    }

    /**
     * Set all personal properties of an XML file for an item (doesn't overwrite
     * existing properties for perfs).
     * 
     * @param attributes :
     * list of attributes for this XML item
     */
    @Override
    public void populateProperties(final Attributes attributes) {
        for (int i = 0; i < attributes.getLength(); i++) {
            final String sProperty = attributes.getQName(i);
            if (!getProperties().containsKey(sProperty)) {
                String sValue = attributes.getValue(i);
                final PropertyMetaInformation meta = getMeta(sProperty);
                // compatibility code for <1.1 : auto-refresh is now a double,
                // no more a boolean
                if (meta.getName().equals(Const.XML_DEVICE_AUTO_REFRESH)
                        && (sValue.equalsIgnoreCase(Const.TRUE) || sValue.equalsIgnoreCase(Const.FALSE))) {
                    if (getType() == Type.DIRECTORY) {
                        sValue = "0.5d";
                    }
                    if (getType() == Type.FILES_CD) {
                        sValue = "0d";
                    }
                    if (getType() == Type.NETWORK_DRIVE) {
                        sValue = "0d";
                    }
                    if (getType() == Type.EXTDD) {
                        sValue = "3d";
                    }
                    if (getType() == Type.PLAYER) {
                        sValue = "3d";
                    }
                }
                try {
                    setProperty(sProperty, UtilString.parse(sValue, meta.getType()));
                } catch (final Exception e) {
                    Log.error(137, sProperty, e);
                }
            }
        }
    }

    /**
     * Refresh : scan the device to find tracks.
     * This method is only called from GUI. auto-refresh uses refreshCommand() directly.
     * 
     * @param bAsynchronous :
     * set asynchronous or synchronous mode
     * @param bAsk whether we ask for fast/deep scan
     * @param bAfterMove whether this is called after a device move
     * @param dirsToRefresh : only refresh specified dirs, or all of them if null
     */
    public void refresh(final boolean bAsynchronous, final boolean bAsk, final boolean bAfterMove,
            final List<Directory> dirsToRefresh) {
        if (bAsynchronous) {
            final Thread t = new Thread("Device Refresh Thread for : " + name) {
                @Override
                public void run() {
                    manualRefresh(bAsk, bAfterMove, false, dirsToRefresh);
                }
            };
            t.setPriority(Thread.MIN_PRIORITY);
            t.start();
        } else {
            manualRefresh(bAsk, bAfterMove, false, dirsToRefresh);
        }
    }

    /**
     * Deep / full Refresh with GUI.
     */
    public void manualRefreshDeep() {
        final Thread t = new Thread("Device Deep Refresh Thread for : " + name) {
            @Override
            public void run() {
                manualRefresh(false, false, true, null);
            }
        };
        t.setPriority(Thread.MIN_PRIORITY);
        t.start();
    }

    /**
     * The refresh itself.
     * 
     * @param bDeepScan whether it is a deep refresh request or only fast
     * @param bManual whether it is a manual refresh or auto
     * @param dirsToRefresh list of the directory to refresh, null if all of them
     * 
     * @return true if some changes occurred in device
     */
    boolean refreshCommand(final boolean bDeepScan, final boolean bManual, List<Directory> dirsToRefresh) {
        try {
            // Check if this device is mounted (useful when called by
            // automatic refresh)
            if (!isMounted()) {
                return false;
            }
            // Check that device is still available
            boolean readyToMount = checkDevice();
            if (!readyToMount) {
                return false;
            }
            bAlreadyRefreshing = true;
            // reporter is already set in case of manual refresh
            if (reporter == null) {
                reporter = new RefreshReporter(this);
            }
            // Notify the reporter of the actual refresh startup
            reporter.refreshStarted();
            lDateLastRefresh = System.currentTimeMillis();
            // check Jajuk is not exiting because a refresh cannot start in
            // this state
            if (ExitService.isExiting()) {
                return false;
            }
            int iNbFilesBeforeRefresh = FileManager.getInstance().getElementCount();
            int iNbDirsBeforeRefresh = DirectoryManager.getInstance().getElementCount();
            int iNbPlaylistsBeforeRefresh = PlaylistManager.getInstance().getElementCount();
            if (bDeepScan && Log.isDebugEnabled()) {
                Log.debug("Starting refresh of device : " + this);
            }
            // Create a directory for device itself and scan files to allow
            // files at the root of the device
            final Directory top = DirectoryManager.getInstance().registerDirectory(this);
            if (!getDirectories().contains(top)) {
                addDirectory(top);
            }
            // Start actual scan
            List<Directory> dirs = null;
            if (dirsToRefresh == null) {
                // No directory specified ? refresh the top directory
                dirs = new ArrayList<Directory>(1);
                dirs.add(top);
            } else {
                dirs = dirsToRefresh;
            }
            for (Directory dir : dirs) {
                scanRecursively(dir, bDeepScan);
            }
            // Force a GUI refresh if new files or directories discovered or have been
            // removed
            if (((FileManager.getInstance().getElementCount() - iNbFilesBeforeRefresh) != 0)
                    || ((DirectoryManager.getInstance().getElementCount() - iNbDirsBeforeRefresh) != 0)
                    || ((PlaylistManager.getInstance().getElementCount() - iNbPlaylistsBeforeRefresh) != 0)) {
                return true;
            }
            return false;
        } catch (final Exception e) {
            // and regular ones logged
            Log.error(e);
            return false;
        } finally {
            // make sure to unlock refreshing even if an error occurred
            bAlreadyRefreshing = false;
            // reporter is null if mount is not mounted due to early return
            if (reporter != null) {
                // Notify the reporter of the actual refresh startup
                reporter.done();
                // Reset the reporter as next time, it could be another type
                reporter = null;
            }
        }
    }

    /**
     * Scan recursively.
     *  
     * @param dir top directory to scan
     * @param bDeepScan whether we want to perform a deep scan (read tags again)
     */
    private void scanRecursively(final Directory dir, final boolean bDeepScan) {
        dir.scan(bDeepScan, reporter);
        if (reporter != null) {
            reporter.updateState(dir);
        }
        final File[] files = dir.getFio().listFiles(UtilSystem.getDirFilter());
        if (files != null) {
            for (final File element : files) {
                // Leave ASAP if exit request
                if (ExitService.isExiting()) {
                    return;
                }
                final Directory subDir = DirectoryManager.getInstance().registerDirectory(element.getName(), dir,
                        this);
                scanRecursively(subDir, bDeepScan);
            }
        }
    }

    /**
     * Sets the url.
     * 
     * @param url The sUrl to set.
     */
    public void setUrl(final String url) {
        sUrl = url;
        setProperty(Const.XML_URL, url);
        fio = new File(url);
        /** Reset files */
        for (final org.jajuk.base.File file : FileManager.getInstance().getFiles()) {
            file.reset();
        }
        /** Reset playlists */
        for (final Playlist plf : PlaylistManager.getInstance().getPlaylists()) {
            plf.reset();
        }
        /** Reset directories */
        for (final Directory dir : DirectoryManager.getInstance().getDirectories()) {
            dir.reset();
        }
        // Reset the root dir
        rootDir = null;
    }

    /**
     * Synchronizing asynchronously.
     * 
     * @param bAsynchronous :
     * set asynchronous or synchronous mode
     */
    public void synchronize(final boolean bAsynchronous) {
        // Check a source device is defined
        if (StringUtils.isBlank((String) getValue(Const.XML_DEVICE_SYNCHRO_SOURCE))) {
            Messages.showErrorMessage(171);
            return;
        }
        final Device device = this;
        if (!device.isMounted()) {
            try {
                device.mount(true);
            } catch (final Exception e) {
                Log.error(11, getName(), e); // mount failed
                Messages.showErrorMessage(11, getName());
                return;
            }
        }
        if (bAsynchronous) {
            final Thread t = new Thread("Device Synchronize Thread") {
                @Override
                public void run() {
                    synchronizeCommand();
                }
            };
            t.setPriority(Thread.MIN_PRIORITY);
            t.start();
        } else {
            synchronizeCommand();
        }
    }

    /**
     * Synchronize action itself.
     */
    void synchronizeCommand() {
        try {
            bAlreadySynchronizing = true;
            long lTime = System.currentTimeMillis();
            int iNbCreatedFilesDest = 0;
            int iNbCreatedFilesSrc = 0;
            lVolume = 0;
            final boolean bidi = getValue(Const.XML_DEVICE_SYNCHRO_MODE).equals(Const.DEVICE_SYNCHRO_MODE_BI);
            // check this device is synchronized
            final String sIdSrc = (String) getValue(Const.XML_DEVICE_SYNCHRO_SOURCE);
            if (StringUtils.isBlank(sIdSrc) || sIdSrc.equals(getID())) {
                // cannot synchro with itself
                return;
            }
            final Device dSrc = DeviceManager.getInstance().getDeviceByID(sIdSrc);
            // perform a fast refresh
            refreshCommand(false, true, null);
            // if bidi sync, refresh the other device as well (new file can
            // have been copied to it)
            if (bidi) {
                dSrc.refreshCommand(false, true, null);
            }
            // start message
            InformationJPanel.getInstance()
                    .setMessage(
                            new StringBuilder(Messages.getString("Device.31")).append(dSrc.getName()).append(',')
                                    .append(getName()).append("]").toString(),
                            InformationJPanel.MessageType.INFORMATIVE);
            // in both cases (bi or uni-directional), make an unidirectional
            // sync from source device to this one
            iNbCreatedFilesDest = synchronizeUnidirectonal(dSrc, this);
            // now the other one if bidi
            if (bidi) {
                iNbCreatedFilesDest += synchronizeUnidirectonal(this, dSrc);
            }
            // end message
            lTime = System.currentTimeMillis() - lTime;
            final String sOut = new StringBuilder(Messages.getString("Device.33"))
                    .append(((lTime < 1000) ? lTime + " ms" : lTime / 1000 + " s")).append(" - ")
                    .append(iNbCreatedFilesSrc + iNbCreatedFilesDest).append(Messages.getString("Device.35"))
                    .append(lVolume / 1048576).append(Messages.getString("Device.36")).toString();
            // perform a fast refresh
            refreshCommand(false, true, null);
            // if bidi sync, refresh the other device as well (new file can
            // have been copied to it)
            if (bidi) {
                dSrc.refreshCommand(false, true, null);
            }
            InformationJPanel.getInstance().setMessage(sOut, InformationJPanel.MessageType.INFORMATIVE);
            Log.debug(sOut);
        } catch (final RuntimeException e) {
            // runtime errors are thrown
            throw e;
        } catch (final Exception e) {
            // and regular ones logged
            Log.error(e);
        } finally {
            // make sure to unlock synchronizing even if an error occurred
            bAlreadySynchronizing = false;
            // Refresh GUI
            ObservationManager.notify(new JajukEvent(JajukEvents.DEVICE_REFRESH));
        }
    }

    /**
     * Synchronize a device with another one (unidirectional).
     * 
     * @param dSrc 
     * @param dest 
     * 
     * @return nb of created files
     */
    private int synchronizeUnidirectonal(final Device dSrc, final Device dest) {
        final Set<Directory> hsSourceDirs = new HashSet<Directory>(100);
        // contains paths ( relative to device) of desynchronized dirs
        final Set<String> hsDesynchroPaths = new HashSet<String>(10);
        final Set<Directory> hsDestDirs = new HashSet<Directory>(100);
        int iNbCreatedFiles = 0;
        List<Directory> dirs = DirectoryManager.getInstance().getDirectories();
        for (Directory dir : dirs) {
            if (dir.getDevice().equals(dSrc)) {
                // don't take desynchronized dirs into account
                if (dir.getBooleanValue(Const.XML_DIRECTORY_SYNCHRONIZED)) {
                    hsSourceDirs.add(dir);
                } else {
                    hsDesynchroPaths.add(dir.getRelativePath());
                }
            }
        }
        for (Directory dir : dirs) {
            if (dir.getDevice().equals(dest)) {
                if (dir.getBooleanValue(Const.XML_DIRECTORY_SYNCHRONIZED)) {
                    // don't take desynchronized dirs into account
                    hsDestDirs.add(dir);
                } else {
                    hsDesynchroPaths.add(dir.getRelativePath());
                }
            }
        }
        // handle known extensions and image files
        final FileFilter filter = new JajukFileFilter(false,
                new JajukFileFilter[] { KnownTypeFilter.getInstance(), ImageFilter.getInstance() });
        for (Directory dir : hsSourceDirs) {
            // give a chance to exit during sync
            if (ExitService.isExiting()) {
                return iNbCreatedFiles;
            }
            boolean bNeedCreate = true;
            final String sPath = dir.getRelativePath();
            // check the directory on source is not desynchronized. If it
            // is, leave without checking files
            if (hsDesynchroPaths.contains(sPath)) {
                continue;
            }
            for (Directory dir2 : hsDestDirs) {
                if (dir2.getRelativePath().equals(sPath)) {
                    // directory already exists on this device
                    bNeedCreate = false;
                    break;
                }
            }
            // create it if needed
            final File fileNewDir = new File(new StringBuilder(dest.getUrl()).append(sPath).toString());
            if (bNeedCreate && !fileNewDir.mkdirs()) {
                Log.warn("Could not create directory " + fileNewDir);
            }
            // synchronize files
            final File fileSrc = new File(new StringBuilder(dSrc.getUrl()).append(sPath).toString());
            final File[] fSrcFiles = fileSrc.listFiles(filter);
            if (fSrcFiles != null) {
                for (final File element : fSrcFiles) {
                    File[] filesArray = fileNewDir.listFiles(filter);
                    if (filesArray == null) {
                        // fileNewDir is not a directory or an error occurred (
                        // read/write right ? )
                        continue;
                    }
                    final List<File> files = Arrays.asList(filesArray);
                    // Sort so files are copied in the filesystem order
                    Collections.sort(files);
                    boolean bNeedCopy = true;
                    for (final File element2 : files) {
                        if (element.getName().equalsIgnoreCase(element2.getName())) {
                            bNeedCopy = false;
                        }
                    }
                    if (bNeedCopy) {
                        try {
                            UtilSystem.copyToDir(element, fileNewDir);
                            iNbCreatedFiles++;
                            lVolume += element.length();
                            InformationJPanel.getInstance().setMessage(
                                    new StringBuilder(Messages.getString("Device.41")).append(dSrc.getName())
                                            .append(',').append(dest.getName())
                                            .append(Messages.getString("Device.42"))
                                            .append(element.getAbsolutePath()).append("]").toString(),
                                    InformationJPanel.MessageType.INFORMATIVE);
                        } catch (final JajukException je) {
                            Messages.showErrorMessage(je.getCode(), element.getAbsolutePath());
                            Messages.showErrorMessage(27);
                            Log.error(je);
                            return iNbCreatedFiles;
                        } catch (final Exception e) {
                            Messages.showErrorMessage(20, element.getAbsolutePath());
                            Messages.showErrorMessage(27);
                            Log.error(20, "{{" + element.getAbsolutePath() + "}}", e);
                            return iNbCreatedFiles;
                        }
                    }
                }
            }
        }
        return iNbCreatedFiles;
    }

    /**
     * Test device accessibility.
     * 
     * @return true if the device is available
     */
    public boolean test() {
        UtilGUI.waiting(); // waiting cursor
        boolean bOK = false;
        boolean bWasMounted = bMounted; // store mounted state of device before
        // mount test
        try {
            if (!bMounted) {
                mount(true);
            }
        } catch (final Exception e) {
            UtilGUI.stopWaiting();
            return false;
        }
        if (getLongValue(Const.XML_TYPE) != 5) { // not a remote device
            final File file = new File(sUrl);
            if (file.exists() && file.canRead()) { // see if the url exists
                // and is readable
                // check if this device was void
                boolean bVoid = true;
                for (org.jajuk.base.File f : FileManager.getInstance().getFiles()) {
                    if (f.getDirectory().getDevice().equals(this)) {
                        // at least one field in this device
                        bVoid = false;
                        break;
                    }
                }
                if (!bVoid) { // if the device is not supposed to be void,
                    // check if it is the case, if no, the device
                    // must not be unix-mounted
                    if (file.list().length > 0) {
                        bOK = true;
                    }
                } else { // device is void, OK we assume it is accessible
                    bOK = true;
                }
            }
        } else {
            bOK = false; // TBI
        }
        // unmount the device if it was mounted only for the test
        if (!bWasMounted) {
            try {
                unmount(false, false);
            } catch (final Exception e1) {
                Log.error(e1);
            }
        }
        UtilGUI.stopWaiting();
        return bOK;
    }

    /**
     * toString method.
     * 
     * @return the string
     */
    @Override
    public String toString() {
        return "Device[ID=" + getID() + " Name=" + getName() + " Type=" + getType().name() + " URL=" + sUrl + "]";
    }

    /**
     * Unmount the device.
     */
    public void unmount() {
        unmount(false, true);
    }

    /**
     * Unmount the device with ejection.
     * 
     * @param bEjection set whether the device must be ejected
     * @param bUIRefresh set whether the UI should be refreshed
     */
    public void unmount(final boolean bEjection, final boolean bUIRefresh) {
        // look to see if the device is already mounted
        if (!bMounted) {
            Messages.showErrorMessage(125); // already unmounted
            return;
        }
        // ask fifo if it doens't use any track from this device
        if (!QueueModel.canUnmount(this)) {
            Messages.showErrorMessage(121);
            return;
        }
        bMounted = false;
        if (bUIRefresh) {
            ObservationManager.notify(new JajukEvent(JajukEvents.DEVICE_UNMOUNT));
        }
    }
}