public class FileManager {

    /**
     * Contains all LXCFiles.
     */
    private final LinkedList<LXCFile> files;
    /**
     * Stores the latest List of available Files for each instance.
     */
    private final HashMap<LXCInstance, List<LXCFile>> recentFileLists;

    /**
     * Creates a new FileManager.
     */
    public FileManager() {
        files = new LinkedList<LXCFile>();
        recentFileLists = new HashMap<LXCInstance, List<LXCFile>>();
    }

    /**
     * Computes incoming TransFileLists.
     *
     * @param receivedList the received list
     * @param sender the origin of this list
     */
    public FileListChangeSet computeFileList(TransFileList receivedList, LXCInstance sender) {
        FileListChangeSet changeSet = new FileListChangeSet();
        List<LXCFile> addList = receivedList.getAll();
        recentFileLists.put(sender, new ArrayList<LXCFile>(addList)); // store a copy!
        Iterator<LXCFile> iter = files.iterator();
        // Remove all files no longer offered by this instance
        int remoteRemoveIndex = -1;//need old indices for gui
        while (iter.hasNext()) {
            LXCFile file = iter.next();
            if (!file.isLocal()) {
                remoteRemoveIndex++;
            }
            if (sender.equals(file.getInstance())) {
                if (!addList.contains(file)) {
                    // no longer offered, remove it, if not download{ing,ed}
                    if (file.getJobs().isEmpty() && !file.isAvailable()) {
                        iter.remove();
                        changeSet.pushRemovedFile(file, remoteRemoveIndex);
                    } else {
                        // keep, ignore in next step:
                        addList.remove(file);
                    }
                } else {
                    // already known, ignore in next step
                    addList.remove(file);
                }
            }
        }

        receivedList.setInstance(sender);
        receivedList.limitTransVersions();
        for (int i = 0; i < addList.size(); i++) {
            changeSet.pushAddedFile(addList.get(i), files.size() + i);
        }
        files.addAll(addList);
        return changeSet;
    }

    /**
     * Removes all LXCFiles offered by the given LXCInstance
     *
     * @param instance the instance which files should be no longer available
     */
    public FileListChangeSet instanceRemoved(LXCInstance instance) {
        FileListChangeSet removals = new FileListChangeSet();
        recentFileLists.remove(instance);
        Iterator<LXCFile> iter = files.iterator();
        int remoteRemoveIndex = -1;
        while (iter.hasNext()) {
            LXCFile file = iter.next();
            if (!file.isLocal()) {
                remoteRemoveIndex++;
            }
            if (file.getInstance().equals(instance)) {
                // only delete if not download{ing,ed}
                if (file.getJobs().isEmpty() && !file.isAvailable()) {
                    iter.remove();
                    removals.pushRemovedFile(file, remoteRemoveIndex);
                }
            }
        }
        return removals;
    }

    /**
     * Returns the local representation for a LXCFile sent by a remote instance.
     * This step is required to upload a file, because only the local representation contains the paths to the source files.
     *
     * @param remoteRepresentation the remote representation
     * @return the local representation, or null if file not available
     */
    public LXCFile localRepresentation(LXCFile remoteRepresentation) {
        int index = files.indexOf(remoteRepresentation);
        if (index != -1) {
            return files.get(index);
        }
        return null;
    }

    /**
     * Creates an up-to-date TransFileList containing all LXCFiles offered by this instance.
     *
     * @return a TransFileList containing all LXCFiles
     */
    public TransFileList getTransFileList() {
        ArrayList<LXCFile> offeredFiles = new ArrayList<LXCFile>();
        for (LXCFile file : files) {
            if (file.isLocal()) {
                offeredFiles.add(file);
            }
        }
        return new TransFileList(offeredFiles);
    }

    /**
     * Returns a list of all known Files.
     * This list is backed by the internal list, but not modifiable.
     *
     * @see java.util.Collections
     * @return a list of all known files, backed but unmodifiable
     */
    public List<LXCFile> getList() {
        return Collections.unmodifiableList(files);
    }

    /**
     * Adds a file offered by the local instance.
     *
     * @param newfile the new file
     * @return index of new file
     */
    public int addLocal(LXCFile newfile) {
        // do not allow duplicates
        for (LXCFile file : files) {
            if (file.equals(newfile)) {
                return -1;
            }
        }
        files.add(newfile);
        return files.size() - 1;
    }

    /**
     * Removes a file offered by the local instance.
     *
     * @param file a file offered by the local instance
     */
    public void removeLocal(LXCFile file) {
        files.remove(file);
    }

    /**
     * Returns true, if there are any transfers running.
     *
     * @return true, if still transferring
     */
    public boolean transferRunning() {
        for (LXCFile file : files) {
            if (!file.getJobs().isEmpty()) {
                return true;
            }
        }
        return false;
    }

    /**
     * Resets a available file.
     * Resetting a file reverts the "downloaded"-flag.
     * If no longer available, the file will disappear.
     * Otherwise, it can be redownloaded.
     *
     * @param file the available, non-local file to reset
     */
    public void resetAvailableFile(LXCFile file) {
        if (file.isAvailable() && !file.isLocal() && !file.isLocked()) {
            file.setAvailable(false);
            if (!recentFileLists.containsKey(file.getInstance()) || !recentFileLists.get(file.getInstance()).contains(file)) {
                files.remove(file);
            }
        } else {
            throw new IllegalArgumentException("Cannot reset given file! (debug: "
                    + file.getShownName() + " " + file.isAvailable() + " "
                    + file.isLocal() + " " + file.isLocked());
        }
    }

    /**
     * Generates a new file name that is currently unused.
     * Collisions are resolved by adding a number after the base name
     * @param base base name, like "image"
     * @param extension extension, without the dot, like "png"
     * @return (probably) unique name like "image05.png"
     */
    public String generateUniqueFileName(String base, String extension) {
        int counter = 0;
        outer:
        while (true) {
            counter++;
            String proposal = base + counter + "." + extension;
            for (LXCFile file : files) {
                if (file.getShownName().equals(proposal)) {
                    continue outer;
                }
            }
            // no such file
            return proposal;
        }
    }
} public class LXCFile implements Serializable {

    static final long serialVersionUID = 2812501218020544733L;
    /**
     * The user wants to share a single file.
     */
    public static final int TYPE_FILE = 1;
    /**
     * The user wants to share a single folder.
     */
    public static final int TYPE_FOLDER = 2;
    /**
     * The user wants to share multiple files (and/or folders).
     */
    public static final int TYPE_MULTI = 3;

    private static final LXCLogger logger = LXCLogBackend.getLogger("file");

    /**
     * Identifier, chosen at random, required for equals.
     */
    public final long id;
    /**
     * If true, the file has been downloaded successfully.
     * Only valid for remote files.
     */
    private transient boolean isAvailable = false;
    /**
     * The LXCInstance that created/offers this file.
     * This is transient, because each running instance of LXC has its own representations of remote instances.
     */
    private transient LXCInstance instance;
    /**
     * If true, this file is locked and cannot be downloaded.
     * This is used to prevent further attempts to download a file while one is still connecting.
     */
    private transient boolean locked = false;
    /**
     * The list of files that the user wants to share.
     * Including directorys, but not the files within them.
     * Can be shared with other classes, because it is unmodifiable.
     */
    private transient List<VirtualFile> files;
    /**
     * The jobs currently assigned to this file.
     */
    private transient List<LXCJob> jobs = new ArrayList<LXCJob>();
    /**
     * The textual representation that is displayed.
     */
    private final String shownName;
    /**
     * The total size in bytes.
     * Is calculated by the constructor.
     * Includes all files in subfolders.
     * Directorys count as zero bytes.
     */
    private long fileSize;
    /**
     * The type of this file.
     * One of TYPE_FILE, TYPE_FOLDER or TYPE_MULTI
     */
    private final int type;
    /**
     * Transport protocol version number.
     */
    private int lxcTransVersion = 1;
    /**
     * The maximum version number supported by this client.
     */
    private final transient int lxcTransVersionMax = 1;
    /**
     * Number of successful transfers.
     */
    private transient int numberOfTransfers = 0;

    /**
     * The new constructor, creates a LXCFile with the given parameters.
     * May take a long time to calculate the size if many files are involved.
     *
     * @param fileList a list of VirtualFiles
     * @param shownN   the name that is shown
     */
    public LXCFile(List<VirtualFile> fileList, String shownN) {
        id = Double.doubleToLongBits(Math.random());
        // create a copy:
        this.files = new ArrayList<VirtualFile>(fileList);
        // Create name
        if (fileList.size() > 1) {
            shownName = shownN.concat(" + " + (fileList.size() - 1));
        } else {
            shownName = shownN;
        }

        // set type
        if (files.size() > 1) {
            type = LXCFile.TYPE_MULTI;
        } else if (files.get(0).isDirectory()) {
            type = LXCFile.TYPE_FOLDER;
        } else {
            type = LXCFile.TYPE_FILE;
        }

        // this constructor only creates local files
        instance = LXCInstance.local;

        // calculate size
        for (VirtualFile file : fileList) {
            fileSize += calcSize(file);
        }
    }

    /**
     * Adds a job to this LXCFile.
     *
     * @param job the new job
     */
    public void addJob(LXCJob job) {
        if (jobs == null) {
            jobs = new ArrayList<LXCJob>();
        }
        jobs.add(job);
    }

    /**
     * Removes a certain job from this LXCFile.
     *
     * @param job     the job to be removed
     * @param success true if transfer was successful
     */
    public void removeJob(LXCJob job, boolean success) {
        jobs.remove(job);
        if (success) {
            numberOfTransfers++;
        }
    }

    /**
     * Returns a List containing all LXCJobs added to this LXCFile.
     *
     * @return a List with all LXCJobs
     */
    public List<LXCJob> getJobs() {
        if (jobs == null) {
            jobs = new ArrayList<LXCJob>();
        }
        return jobs;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        LXCFile lxcFile = (LXCFile) o;

        return id == lxcFile.id;
    }

    @Override
    public int hashCode() {
        return (int) (id ^ (id >>> 32));
    }

    /**
     * Sets the available-flag.
     *
     * @param available new state of the flag
     */
    public void setAvailable(boolean available) {
        isAvailable = available;
    }

    /**
     * Computes the size of a given VirtualFile.
     * Recursively scans directorys.
     * Only counts files. Directorys count as zero bytes.
     *
     * @param startFile a file to scan
     * @return the size of the file/dir+subs in bytes
     */
    private long calcSize(VirtualFile startFile) {
        long size = 0;
        LinkedList<VirtualFile> queue = new LinkedList<VirtualFile>();
        // init queue
        queue.addLast(startFile);
        // add everything up
        while (!queue.isEmpty()) {
            VirtualFile file = queue.pollFirst();
            if (file.isDirectory()) {
                queue.addAll(file.children());
            } else {
                size += file.size();
            }
        }
        return size;
    }

    /**
     * Returns the available-flag.
     *
     * @return the available-flag
     */
    public boolean isAvailable() {
        return isAvailable;
    }

    /**
     * Returns the shown name.
     *
     * @return the shown name
     */
    public String getShownName() {
        return shownName;
    }

    /**
     * Returns the type.
     *
     * @return the type
     */
    public int getType() {
        return type;
    }

    /**
     * Returns the size of this file.
     *
     * @return the size of this file
     */
    public long getFileSize() {
        return fileSize;
    }

    /**
     * Returns the LXCInstance that created/offers this file.
     *
     * @return the instance
     */
    public LXCInstance getInstance() {
        return instance;
    }

    /**
     * Sets the LXCInstance that offers this file.
     *
     * @param instance the instance to set
     */
    public void setInstance(LXCInstance instance) {
        this.instance = instance;
    }

    /**
     * Returns the locked-flag.
     *
     * @return the locked-flag
     */
    public boolean isLocked() {
        return locked;
    }

    /**
     * Sets the locked-flag.
     *
     * @param locked the new state
     */
    public void setLocked(boolean locked) {
        this.locked = locked;
    }

    /**
     * Returns the list of files the user wants to share.
     *
     * @return the file-list
     */
    public List<VirtualFile> getFiles() {
        return files;
    }

    /**
     * Sets the list of base files.
     * This is required after downloads.
     *
     * @param baseFiles the base file list
     */
    public void setBaseFiles(List<VirtualFile> baseFiles) {
        this.files = baseFiles;
    }

    /**
     * Returns the current set protocol version.
     *
     * @return the lxcTransVersion
     */
    public int getLxcTransVersion() {
        return lxcTransVersion;
    }

    /**
     * Limits the protocol version to the maximum supported by this local client.
     */
    public void limitTransVersion() {
        if (lxcTransVersion > lxcTransVersionMax) {
            lxcTransVersion = lxcTransVersionMax;
        }
    }

    /**
     * Returns true, if this file is offered by this local instance.
     * Returns false, if received from a remote instance.
     *
     * @return true, if local
     */
    public boolean isLocal() {
        return instance.id == LXCInstance.local.id;
    }

    /**
     * Returns the number of (completed) transfers.
     *
     * @return number of transfers
     */
    public int getNumberOfTransfers() {
        return numberOfTransfers;
    }

    /**
     * Converts a list of real files to a List of VirtualFiles.
     * Determines the common toplevel node, if any, and creates file accordingly.
     *
     * @param files a list of real (java.io.File) files
     * @return a list of VirtualFiles
     */
    public static List<VirtualFile> convertToVirtual(List<File> files) {
        // Search toplevel node(s):
        LinkedList<File> topLevelNodes = new LinkedList<File>();
        outer:
        for (File file : files) {
            // Check if this file is below any toplevel node
            for (File topLevelNode : topLevelNodes) {
                if (file.getAbsolutePath().startsWith(topLevelNode.getAbsolutePath() + File.separator)) {
                    // below - this means file is not a toplevel node
                    continue outer;
                }
            }
            // This file is a toplevel node - check if this makes other toplevel nodes obsolete
            for (int i = 0; i < topLevelNodes.size(); i++) {
                File topLevelNode = topLevelNodes.get(i);
                if (topLevelNode.getAbsolutePath().startsWith(file.getAbsolutePath() + File.separator)) {
                    // the new file is a parent of this toplevel node
                    topLevelNodes.remove(i);
                }
            }
            // Insert the new toplevel node
            topLevelNodes.add(file);
        }

        ArrayList<VirtualFile> result = new ArrayList<VirtualFile>();
        // Each of these files now either has a parent in topLevelNodes or is a toplevel node itself.
        for (File file : files) {
            if (topLevelNodes.contains(file)) {
                // toplevel
                result.add(new RealFile(file));
            } else {
                // search topLevelNode
                for (File topLevelNode : topLevelNodes) {
                    if (file.getAbsolutePath().startsWith(topLevelNode.getAbsolutePath() + File.separator)) {
                        // found toplevel node
                        result.add(new RealFile(file, topLevelNode));
                        break;
                    }
                }
                // this should never be reached
                logger.warn("Cannot find TopLevel node for " + file.getAbsolutePath());
                // just assume this is toplevel as well
                result.add(new RealFile(file));
            }
        }
        return result;
    }

    /**
     * Computes a human-readable String which approximates the value passed to this method.
     *
     * @param fSize the fileSize in bytes
     * @return a human-readable String containing the size (like 12,3 MiB)
     */
    public static String getFormattedSize(long fSize) {
        DecimalFormat form = new DecimalFormat("0.0");
        if (fSize < 1024) {
            return fSize + " B";
        } else if (fSize < 1048576) {
            String size = form.format(fSize / 1024d);
            return size + " KiB";
        } else if (fSize < 1073741824) {
            String size = form.format(fSize / 1048576d);
            return size + " MiB";
        } else if (fSize < 1099511627776L) {
            String size = form.format(fSize / 1073741824d);
            return size + " GiB";
        } else if (fSize < 1125899906842625L) {
            String size = form.format(fSize / 1099511627776d);
            return size + " TiB";
        } else {
            return "unknown";
        }
    }
} public class LXCJob {

    /**
     * If this job represents a running upload (true) or download (false).
     */
    private boolean isSeeder = false;
    /**
     * The remote LXCInstance.
     */
    private final LXCInstance remote;
    /**
     * The corresponding Transceiver.
     */
    private final Transceiver trans;

    /**
     * Creates a new LXCJob with the given parameters.
     *
     * @param trans the running transceiver that transfers the data
     * @param remote the connected remote
     */
    public LXCJob(Transceiver trans, LXCInstance remote) {
        this.trans = trans;
        this.remote = remote;
        isSeeder = trans instanceof Seeder;
    }

    /**
     * Returns true, if this job represents a running upload.
     *
     * @return true if upload, false if download
     */
    public boolean isIsSeeder() {
        return isSeeder;
    }

    /**
     * Returns the remote LXCInstance.
     *
     * @return the remote LXCInstance
     */
    public LXCInstance getRemote() {
        return remote;
    }

    /**
     * Returns the running Transceiver.
     *
     * @return the Transceiver
     */
    public Transceiver getTrans() {
        return trans;
    }

    /**
     * Immediately aborts the transfer.
     */
    public void abortTransfer() {
        trans.abort();
    }
} public final class LXCLogBackend {

    /**
     * Marker String that is printed when the configured log file size limit is reached.
     */
    private static final String logFullMessage = "---- LXCLog: Suspended log output, hard size limit reached. ----";

    /**
     * Number of bytes that can be written before the max log file size is reached.
     *
     * This limit is ignored in debug mode
     */
    private static int byteLimit;
    /**
     * How many old log files to keep.
     */
    private static int logRotationCopies = 3;

    /**
     * The actual, backend printer.
     */
    private static ModifiableSinkPrintStream printer = new ModifiableSinkPrintStream(System.out);

    private LXCLogBackend() {}

    public static void init(File targetDir, int logSizeLimit, int logRotationCopies, boolean debug) {
        // if debug is set, just send everything to system.out so the IDE sees it
        // note: debug mode ignores the log file size limit
        if (debug) {
            System.out.println("LXCLog: Debug mode, logging everything to System.out");
            printer.switchOutputStream(System.out);// re-set to System.out, may have changed since init
            return;
        }
        if (targetDir == null || logSizeLimit <= 0) {
            throw new IllegalArgumentException("targetDir cannot be null, logSizeLimit must be greater than zero");
        }

        byteLimit = logSizeLimit;
        LXCLogBackend.logRotationCopies = logRotationCopies;

        boolean logOK = true;
        File logFile = createFile(targetDir);
        if (logFile == null) {
            logOK = false;
        } else {
            try {
                PrintStream sizeLimitedPrinter = createSizeLimitPrinter(logFile);
                printer.switchOutputStream(sizeLimitedPrinter);
            } catch (FileNotFoundException ex) {
                logOK = false;
            }
        }

        if (!logOK) {
            // continue to run and send everything to system.out
            System.err.println("LXCLog: Cannot initialize logging in dir \"" + targetDir.getAbsolutePath() +
                    "\", sending all log content to System.out instead.");
            printer.switchOutputStream(System.out);
        }
    }

    public static LXCLogger getLogger(String from) {
        // format from so it has 16 chars
        from = String.format("%16.16s", from);
        return new LXCLogger(printer, from);
    }

    private static File createFile(File targetDir) {
        File current = new File(targetDir, "lxc.log");
        // handle log rotation
        if (current.exists() && logRotationCopies > 0) {
            for (int i = logRotationCopies - 1; i >= 1; i--) {
                File rotateDown = new File(targetDir,"lxc_oldlog" + i + ".log");
                File rotateVictim = new File(targetDir,"lxc_oldlog" + (i + 1) + ".log");
                if (rotateDown.exists()) {
                    if (rotateVictim.exists()) {
                        rotateVictim.delete();
                    }
                    rotateDown.renameTo(rotateVictim);
                }
            }
            File previous = new File(targetDir,"lxc_oldlog1.log");
            if (previous.exists()) {
                previous.delete();
            }
            current.renameTo(previous);
        }
        if (!current.exists()) {
            try {
                current.createNewFile();
            } catch (IOException ex) {
                System.err.println("Cannot write to logfile \"lxc.log\"");
            }
        }
        if (!current.canWrite()) {
            System.err.println("Cannot write to logfile \"lxc.log\"");
            return null;
        }
        return current;
    }

    private static PrintStream createSizeLimitPrinter(File logFile) throws FileNotFoundException {
        final FileOutputStream fileOut = new FileOutputStream(logFile);
        return new PrintStream(new ByteLimitOutputStream(fileOut, byteLimit, new Runnable() {

            boolean notePrinted = false;

            @Override
            public void run() {
                // when limit is reached, print notice in file once, bypassing the limited stream
                if (notePrinted) {
                    return;
                }
                notePrinted = true;
                try {
                    fileOut.write(logFullMessage.getBytes());
                } catch (IOException ex) {
                    // ignore
                }
            }
        }));
    }

} public class LXCLogger {

    private static final String CODE_ASSERT = "ASRT";
    private static final String CODE_ERROR = "ERR ";
    private static final String CODE_WARN = "WARN";
    private static final String CODE_INFO = "INFO";

    private static final String EXCEPTION_SPACER = "                                           "; // 44*" "

    private final PrintStream backend;
    private final PrintStream exceptionPrinter;
    private final String from;

    /**
     * Internal constructor, use LXCLogBackend.getLogger() to get a logger.
     */
    LXCLogger(PrintStream backend, String from) {
        this.backend = backend;
        this.from = from;
        exceptionPrinter = new PrintStream(backend) {
            @Override
            public void println(Object o) {
                // this prints a large whitespace before each exception line to fix the alignment
                // best-effort only, as this relies on the internal implementation of printStackTrace to only use this
                // particular method
                super.print(EXCEPTION_SPACER);
                super.println(o);
            }
        };
    }

    public void info(String message) {
        log(CODE_INFO, message);
    }

    public void info(String message, Throwable cause) {
        log(CODE_INFO, message, cause);
    }

    public void warn(String message) {
        log(CODE_WARN, message);
    }

    public void warn(String message, Throwable cause) {
        log(CODE_WARN, message, cause);
    }

    public void error(String message) {
        log(CODE_ERROR, message);
    }

    public void error(String message,  Throwable cause) {
        log(CODE_ERROR, message, cause);
    }

    public void wtf(String message) {
        log(CODE_ASSERT, message);
    }

    public void wtf(Throwable cause) {
        // this is the only loglevel that allows exceptions without a message
        wtf("Assertion Error", cause);
    }

    public void wtf(String message,  Throwable cause) {
        log(CODE_ASSERT, message, cause);
    }

    private void log(String code, String message) {
        log(code, message, null);
    }

    private void log(String code, String message, Throwable cause) {
        backend.println(code + " " + getTime() + " " + from + ": " + message);
        if (cause != null) {
            cause.printStackTrace(exceptionPrinter);
        }
    }

    private String getTime() {
        // in the future, when java.time is available on android (coming with Android O)
        // return ZonedDateTime.now().format(DateTimeFormatter.ISO_INSTANT)

        // until then, we use java.sql.Timestamp. Not exactly faster than SimpleDateFormat, but at least thread safe
        return new Timestamp(System.currentTimeMillis()).toString().substring(0, 19); // cut off milliseconds
    }
} public class Leecher extends Transceiver {

    private final LXCLogger logger;
    /**
     * The folder to save the loaded files into.
     */
    private final File targetFolder;

    @Override
    public void run() {
       logger.info("Starting transmission... (version " + transVersion + ")");

        long startTime = System.currentTimeMillis();
        List<VirtualFile> baseFiles = new ArrayList<VirtualFile>();

        try {

            while (true) {
                byte cmd = in.readByte();

                if (cmd == 'f' || cmd == 'F') {
                    // next file
                    // read date if version >= 1
                    long date = file.getLxcTransVersion() >= 1 ? in.readLong() : System.currentTimeMillis();
                    // read size
                    long size = in.readLong();
                    // read path
                    String path = in.readUTF();
                    // convert path
                    path = path.replace("/", File.separator);
                    path = path.replace("\\", File.separator);
                    // create file / parent folders
                    File target = new File(targetFolder, path);
                    target.getParentFile().mkdirs();
                    // track base files
                    if (cmd == 'F') {
                        baseFiles.add(new RealFile(target));
                    }
                    // transfer file content (real data)
                    BufferedOutputStream fileout;
                    byte[] buffer = new byte[4096];
                    try {
                        fileout = new BufferedOutputStream(new FileOutputStream(target), 8388608);
                        while (size > 0) {
                            int read;
                            if (size >= buffer.length) {
                                read = in.read(buffer);
                            } else {
                                read = in.read(buffer, 0, (int) size);
                            }
                            if (read == -1) {
                                // EOF
                                throw new EOFException("Sudden EOF, aborting.");
                            }
                            fileout.write(buffer, 0, read);
                            size -= read;
                            transferredBytes += read;
                            updateProgress();
                        }
                        fileout.flush();
                        fileout.close();
                        target.setLastModified(date);
                    } catch (FileNotFoundException ex) {
                        // cannot create file, possible reasons are:
                        // - name invalid on this file system
                        // - trying to create file, but already exists as dir
                        // - no write permission
                        //
                        // abort
                        logger.error("Cannot create file \"" + target.getAbsolutePath() + "\", aborting transfer", ex);
                        listener.finished(false, false, false, target.getAbsolutePath());
                        break;
                    } catch (IOException ex) {
                        if (!abort) {
                            logger.error("Unexpected IOException", ex);
                        }
                    }
                } else if (cmd == 'd' || cmd == 'D') {
                    // folder
                    String path = in.readUTF();
                    // convert path
                    path = path.replace("/", File.separator);
                    path = path.replace("\\", File.separator);
                    // create folder
                    File target = new File(targetFolder, path);
                    target.mkdirs();
                    // track base dirs
                    if (cmd == 'D') {
                        baseFiles.add(new RealFile(target));
                    }
                } else if (cmd == 'e') {
                    // done
                    logger.info("Finished in " + (System.currentTimeMillis() - startTime) + "ms, speed was " + String.format(Locale.US, "%.2f", 1.0 * totalBytes / (System.currentTimeMillis() - startTime)) + "kb/s");
                    logger.info("Done receiving.");
                    // set base files
                    file.setBaseFiles(baseFiles);
                    listener.finished(true, false, false, null);
                    break;
                } else if (cmd == 's') {
                    // error, client no longer has the file
                    logger.warn("Remote reports missing file, aborting transfer.");
                    listener.finished(false, false, true, null);
                    break;
                } else {
                    logger.warn("Unused command: " + cmd);
                }
            }

        } catch (IOException ex) {
            if (!abort) {
                logger.error("Unexpected IOException(2)", ex);
            }
            listener.finished(false, abort,false, null);
        } finally {
            try {
                in.close();
            } catch (Exception ex) {
                // ignore
            }
            try {
                out.close();
            } catch (Exception ex) {
                // ignore
            }
            try {
                socket.close();
            } catch (Exception ex) {
                // ignore
            }
            // force immediate release of file handles
            System.gc();
        }
    }

    /**
     * Creates a new Leecher with the given parameters.
     * It is assumed the Streams as fully established
     * This means in particular:
     * - Both Streams are connected to a remote Seeder
     * - The remote side has been preconfigured and can start receiving files immediately
     * Please note:
     * This implementation uses two streams (intput+output) for Seeder and Leechers.
     * Obviously only one stream should be required (output for seed, input for leech)
     * Removing the second stream would break compatibilty with previous versions, therefore it is kept.
     * However, it should not be used and may be removed in the future.
     *
     * @param socket the socket
     * @param output the OutputStream, connected and ready
     * @param input the InputStream, connected and ready
     * @param transFile the {@link LXCFile} that is to be transferred
     * @param targetFolder the folder to save the files into
     * @param transVersion version of the transfer protocol {@link Transceiver}
     */
    public Leecher(Socket socket, ObjectInputStream input, ObjectOutputStream output, LXCFile transFile, File targetFolder, int transVersion) {
        this.logger = LXCLogBackend.getLogger("leech-" + transFile.id);
        this.socket = socket;
        this.file = transFile;
        this.out = output;
        this.in = input;
        this.targetFolder = targetFolder;
        this.transVersion = transVersion;

        this.totalBytes = file.getFileSize();
    }

    @Override
    public void start() {
        Thread thread = new Thread(this);
        thread.setName("leecher_" + file.id);
        thread.start();
    }

    @Override
    public void abort() {
        // Just kill it
        logger.info("Aborting download upon user-request. Exceptions may occur!");
        abort = true;
        try {
            in.close();
        } catch (Exception ex) {
            // ignore
        }
    }
} public class Seeder extends Transceiver {

    private final LXCLogger logger;

    @Override
    public void run() {
        logger.info("Starting transmission... (version " + transVersion + ")");
        long startTime = System.currentTimeMillis();
        boolean transferOk = true;
        boolean fileBroken = false;
        try {
            List<VirtualFile> files = file.getFiles();
            ArrayList<VirtualFile> allList = new ArrayList<VirtualFile>();
            allList.addAll(files);

            for (int i = 0; i < allList.size(); i++) {
                VirtualFile currentFile = allList.get(i);
                if (currentFile.isDirectory()) {
                    // add files recursively
                    allList.addAll(currentFile.children());
                    // send directory itself
                    out.writeByte(files.contains(currentFile) ? 'D' : 'd');
                    out.writeUTF(currentFile.getTransferPath());
                } else {
                    // transfer a file
                    try {
                        InputStream rawInput = currentFile.getInputStream();
                        // inform client
                        out.writeByte(files.contains(currentFile) ? 'F' : 'f');
                        // send date, if version >= 1
                        if (transVersion >= 1) {
                            out.writeLong(currentFile.lastModified());
                        }
                        // send size
                        out.writeLong(currentFile.size());
                        // send path
                        out.writeUTF(currentFile.getTransferPath());
                        out.flush();
                        // send content (real data)
                        BufferedInputStream filein = null;
                        try {
                            filein = new BufferedInputStream(rawInput, 8388608);
                            byte[] buffer = new byte[4096];
                            int gotbytes;
                            while ((gotbytes = filein.read(buffer)) > 0) {
                                transferredBytes += gotbytes;
                                updateProgress();
                                out.write(buffer, 0, gotbytes);
                                out.flush();
                            }
                            // done
                            out.flush();
                        } catch (FileNotFoundException ex) {
                            logger.error("Unexpected FileNotFoundException during transfer", ex);
                        } finally {
                            try {
                                if (filein != null) {
                                    filein.close();
                                }
                            } catch (IOException ex) {
                                // ignore
                            }
                        }
                    } catch (FileNotFoundException ex) {
                        logger.error("Cannot upload file with transferpath \"" + currentFile.getTransferPath() + "\", file no longer exists - aborting transfer.");
                        // Cannot read file, send sorry (newer clients (>=149) display a message; older clients silently discard this.
                        out.writeByte('s');
                        out.flush();
                        // Abort transfer gracefully
                        transferOk = false;
                        fileBroken = true;
                        break;
                    }
                }
            }
            // transfer complete
            out.writeByte('e');
            out.flush();
        } catch (IOException ex) {
            if (!abort) {
                logger.error("Unexpected IException", ex);
            }
            transferOk = false;
        } finally {
            try {
                out.close();
                in.close();
                socket.close();
                System.gc(); // Closes filehandles etc
            } catch (Exception ex) {
                // Who cares..
            }
        }

        if (transferOk) {
            logger.info("Finished in " + (System.currentTimeMillis() - startTime) + "ms, speed was " + String.format(Locale.US, "%.2f", 1.0 * totalBytes / (System.currentTimeMillis() - startTime)) + "kb/s");
            listener.finished(true, abort, false, null);
            logger.info("Done seeding.");
        } else {
            listener.finished(false, abort, fileBroken,null);
            logger.error("Lost connection, aborting.");
        }

    }

    /**
     * Creates a new Seeder with the given parameters.
     * It is assumed the Streams are fully established
     * This means in particular:
     * - Both Streams are connected to a remote Leecher
     * - The remote side has been preconfigured and can start receiving files immediately
     * Please note:
     * This implementation uses two streams (intput+output) for Seeder and Leechers.
     * Obviously only one stream should be required (output for seed, input for leech)
     * Removing the second stream would break compatibilty with previous versions, therefore it is kept.
     * However, it should not be used and may be removed in the future.
     *
     * @param socket the socket
     * @param output the OutputStream, connected and ready
     * @param input the InputStream, connected and ready
     * @param transFile the {@link LXCFile} that is to be transferred
     * @param transVersion version of the transfer protocol {@link Transceiver}
     * @see Transceiver
     */
    public Seeder(Socket socket, ObjectOutputStream output, ObjectInputStream input, LXCFile transFile, int transVersion) {
        logger = LXCLogBackend.getLogger("seed-" + transFile.id);
        this.socket = socket;
        this.file = transFile;
        this.out = output;
        this.in = input;
        this.transVersion = transVersion;
        this.totalBytes = file.getFileSize();
    }

    /**
     * Starts this Leecher.
     * Creates a new thread, returns immediately.
     */
    @Override
    public void start() {
        Thread thread = new Thread(this);
        thread.setName("seeder_" + file.getShownName());
        thread.start();
    }

    @Override
    public void abort() {
        // Just kill it
        logger.info("Leecher: Aborting upload upon user-request.");
        abort = true;
        try {
            in.close();
        } catch (Exception ex) {
            // ignore
        }
    }
} public abstract class Transceiver implements Runnable {

    /**
     * The LXCFile that is to be transferred.
     */
    protected LXCFile file;
    /**
     * The total number of bytes to be transferred.
     */
    protected long totalBytes;
    /**
     * The current number of transferred bytes.
     */
    protected long transferredBytes = 0;
    /**
     * Which protocol version to use.
     */
    protected int transVersion;
    /**
     * Silences exceptions after user-triggered transfer abort.
     */
    protected boolean abort = false;
    /**
     * Buffer for five speed-measurements.
     */
    private final int[] lastSpeeds = new int[5];
    /**
     * When the last speed measurement was conducted
     */
    private long lastSpeedMeasureTime = System.currentTimeMillis();
    /**
     * Number of transferred bytes @ lastSpeedMeasureTime
     */
    private long lastTransferredBytes = 0;
    /**
     * A mod-5-counter.
     * Used for the rolling-average speed-calculations
     */
    private int nextArrayPos = 0;
    /**
     * The OutputStream.
     * Implementations must create and connect this stream.
     */
    protected ObjectOutputStream out;
    /**
     * The InputStream.
     * Implementations must create and connect this stream.
     */
    protected ObjectInputStream in;
    /**
     * The socket for the transfer.
     * Since the streams are created externally, this socket is not required for the transfer itself, but it has to be closed somewhere.
     */
    protected Socket socket;
    /**
     * Used to trigger gui-updates on progress.
     */
    protected TransceiverListener listener;
    /**
     * Used to display information about transfer progress to the user.
     */
    private ProgressIndicator progressIndicator;

    /**
     * Sets the Listener.
     * Must be called before any other methods!
     *
     * @param listener the new listener
     */
    public void setListener(TransceiverListener listener) {
        this.listener = listener;
    }

    /**
     * Returns the current progress of this filetransfer in percent
     *
     * @return the current progress of this filetransfer in percent
     */
    public int getProgress() {
        return (int) (100f * transferredBytes / totalBytes);
    }

    /**
     * Returns the current transfer speed.
     *
     * @return the current transfer speed
     */
    public int getCurrentSpeed() {
        updateSpeeds();
        int avg = lastSpeeds[0] + lastSpeeds[1] + lastSpeeds[2] + lastSpeeds[3] + lastSpeeds[4];
        avg /= 5;
        return avg;
    }

    /**
     * Sets the ProgressIndicator used to display progress of running transfers.
     * Overwrites the old ProgressIndicator, if any.
     *
     * @param progressIndicator the new ProgressIndicator
     */
    public void setProgressIndicator(ProgressIndicator progressIndicator) {
        this.progressIndicator = progressIndicator;
    }

    public ProgressIndicator getProgressIndicator() {
        return progressIndicator;
    }

    /**
     * Called by the transfer routine to signal a progressing transfer.
     */
    protected void updateProgress() {
        if (progressIndicator != null) {
            progressIndicator.update(getProgress());
        }
    }

    /**
     * Recalculates the current speed.
     * Takes a new measurement each time it is called (max 2x per sec).
     */
    private void updateSpeeds() {
        long diffT = System.currentTimeMillis() - lastSpeedMeasureTime;
        if (diffT > 500) {
            long diffB = transferredBytes - lastTransferredBytes;

            lastSpeeds[nextArrayPos] = (int) (diffB * 1000 / diffT);

            lastSpeedMeasureTime = System.currentTimeMillis();
            lastTransferredBytes = transferredBytes;

            // increment rolling counter
            if (++nextArrayPos >= 5) {
                nextArrayPos = 0;
            }
        }
    }

    /**
     * Aborts the transfer.
     * Has no effect if the transfer is already finished.
     */
    public abstract void abort();

    /**
     * Starts this Transceiver.
     * Creates a new thread, returns immediately.
     */
    public abstract void start();
} public interface TransceiverListener {

    /**
     * Called by Transceiver upon completion of the download/upload.
     * @param success true, if successful, false if cancelled/interrupted
     * @param cancelled true, if the job was cancelled by the user (i.e. don't display any error messages)
     * @param removeFile if true, the error was severe and the file should no longer be offered (only used by seeders)
     * @param problemFilePath path of problematic file, if a file caused a transfer abort/cancel (null otherwise)
     */
    public void finished(boolean success, boolean cancelled, boolean removeFile, String problemFilePath);
} public class FileServer implements Runnable {

    private final LXCLogger logger;
    /**
     * The listener to pass events to.
     */
    private FileServerListener listener;
    /**
     * The FileManager to get filelists etc from.
     */
    private FileManager fileManager;
    /**
     * The socket this server uses to listen to illegal commands.
     */
    private ServerSocket servSock;

    /**
     * Creates a new FileServer with the given parameters.
     *
     * @param listener the listener to pass events to
     * @param fileManager the filemanager, used to check if files are available for download
     */
    public FileServer(FileServerListener listener, FileManager fileManager) {
        this.logger = LXCLogBackend.getLogger("file-server");
        this.listener = listener;
        this.fileManager = fileManager;
    }

    @Override
    public void run() {
        try {
            servSock = new ServerSocket();
            servSock.setReceiveBufferSize(212992);
            servSock.setPerformancePreferences(0, 0, 1);
            servSock.bind(new InetSocketAddress(27719));

            while (true) {
                Socket client;
                try {
                    client = servSock.accept();
                } catch (SocketException ex) {
                    // servSock was closed, ignore
                    break;
                }

                client.setSendBufferSize(212992);
                ObjectInputStream input;
                ObjectOutputStream output;
                try {
                    output = new ObjectOutputStream(new BufferedOutputStream(client.getOutputStream()));
                    output.flush();
                    input = new LookaheadObjectInputStream(new BufferedInputStream(client.getInputStream()));
                    try {
                        LXCFile file = (LXCFile) input.readObject();
                        if (file != null) {
                            // get version number
                            int version = file.getLxcTransVersion();
                            // file still available?
                            file = fileManager.localRepresentation(file);
                            if (file != null && file.isLocal()) {
                                // send ACK, start transfer
                                output.writeByte('y');
                                output.flush();
                                listener.downloadRequest(client, file, output, input, client.getInetAddress(), version);
                            } else {
                                // refuse request
                                output.writeByte('n');
                                output.flush();
                            }
                        }
                    } catch (ClassNotFoundException | ClassCastException ex) {
                        logger.warn("Cannot read incoming file request", ex);
                    }
                } catch (IOException ex) {
                    logger.warn("Problem receiving file request", ex);
                }
            }

        } catch (Exception ex) {
            logger.error("Unrecoverable problem", ex);
        }
    }

    /**
     * Starts this server.
     */
    public void start() {
        Thread thread = new Thread(this);
        thread.setDaemon(true);
        thread.setName("fileserver");
        thread.start();
    }

    /**
     * Stop this server.
     */
    public void stop() {
        try {
            servSock.close();
        } catch (IOException ex) {
            // ignore
        }
    }
} public interface FileServerListener {

    /**
     * Called upon download-requests.
     * Connection is already established, Seeder should be startet.
     *
     * @param socket the socket
     * @param file the requested LXCFile
     * @param outStream the outputStream
     * @param inStream the inputStream
     * @param address the remote address
     * @param transVersion protocol version to use
     */
    public void downloadRequest(Socket socket, LXCFile file, ObjectOutputStream outStream, ObjectInputStream inStream, InetAddress address, int transVersion);
} public class ListServer implements Runnable {

    private LXCLogger logger;
    /**
     * The ServerSocket used.
     */
    private ServerSocket servSock;
    /**
     * The listener, used to deliver incoming filelists.
     */
    private final ListServerListener listener;

    @Override
    public void run() {
        try {
            while (true) {
                Socket client;
                try {
                    client = servSock.accept();
                } catch (SocketException ex) {
                    // servSock was closed, ignore
                    break;
                }

                // Wait for next list, blocks
                try {
                    ObjectInputStream input = new LookaheadObjectInputStream(client.getInputStream());
                    TransFileList list = (TransFileList) input.readObject();
                    if (list != null) {
                        listener.listReceived(list, client.getInetAddress());
                    } else {
                        // List-request
                        listener.listRequested();
                    }
                } catch (ClassNotFoundException | ClassCastException | IOException ex) {
                    logger.warn("Unable to receive list from " + client.getInetAddress(), ex);
                } finally {
                    closeSocket(client);
                }
            }
        } catch (Exception ex) {
            logger.error("Unrecoverable problem", ex);
        }
    }

    private void closeSocket(Socket socket) {
        if (socket != null) {
            try {
                socket.close();
            } catch (IOException ex) {
                // ignore
            }
        }
    }

    /**
     * Binds the Socket to port 27717.
     * Used for the single-instance-check.
     *
     * @throws BindException most likely means LanXchange is already running
     */
    private void bindSocket() throws BindException {
        try {
            servSock = new ServerSocket(27717);
        } catch (IOException ex) {
            if (ex instanceof BindException) {
                throw new BindException();
            }
        }
    }

    /**
     * Creates a new ListServer with the given parameters.
     *
     * @param listener the listener used to deliver incoming filelists
     */
    public ListServer(ListServerListener listener) {
        this.logger = LXCLogBackend.getLogger("list-server");
        this.listener = listener;
    }

    /**
     * Starts the ListServer.
     * Binds to port 27717.
     *
     * @throws BindException if port is already occupied
     */
    public void start() throws BindException {
        Thread thread = new Thread(this);
        thread.setDaemon(true);
        thread.setName("listserver");
        this.bindSocket();
        thread.start();
    }

    public void stop() {
        try {
            servSock.close();
        } catch (IOException ex) {
            // ignore
        }
    }
} public interface ListServerListener {

    /**
     * ListServer received a (new) filelist.
     *
     * @param list the list
     * @param host the origin of this list
     */
    public void listReceived(TransFileList list, InetAddress host);

    /**
     * Someone manually requested the filelist.
     * This will result in a broadcast to everyone
     */
    public void listRequested();
} public class PingServer {

    private final LXCLogger logger;
    /**
     * The sockets currently in use.
     */
    private final Map<NetworkInterface, InterfaceHandler> listenSockets;
    /**
     * The Listener to call when pings are received.
     */
    private final PingServerListener listener;

    /**
     * Creates a new PingServer.
     * PingServers listen for Ping/KeepAlive-Signals by other instances.
     *
     * @param listener callback listener
     */
    public PingServer(PingServerListener listener) {
        logger = LXCLogBackend.getLogger("ping-server");
        this.listener = listener;
        listenSockets = new ConcurrentHashMap<NetworkInterface, InterfaceHandler>();
    }

    /**
     * Sets the list of used interfaces.
     *
     * @param interf the list
     */
    public void updateInterfaces(List<NetworkInterface> interf) {
        // close interfaces that should no longer be used
        ArrayList<NetworkInterface> removeList = new ArrayList<NetworkInterface>();
        for (NetworkInterface inter : listenSockets.keySet()) {
            if (!interf.contains(inter)) {
                removeList.add(inter);
            }
        }
        // remove sockets now (mark+sweep-method avoids concurrentmodificationexception)
        for (NetworkInterface inter : removeList) {
            listenSockets.get(inter).close();
            listenSockets.remove(inter);
        }
        // open new interfaces
        for (NetworkInterface inter : interf) {
            if (!listenSockets.containsKey(inter)) {
                listenTo(inter);
            }
        }
    }

    /**
     * Starts a thread to listen to the given NetworkInterface.
     *
     * @param inter the NetworkInterface to listen to
     */
    private void listenTo(final NetworkInterface inter) {
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    InterfaceHandler handler = new InterfaceHandler(inter);
                    listenSockets.put(inter, handler);
                    handler.handle(inter);
                } catch (SocketException ex) {
                    logger.info("No longer listening to " + inter.getName());
                } catch (IOException ex) {
                    logger.warn("Cannot listen to interface \"" + inter.getDisplayName() + "\"");
                }
            }
        }, "lxc_udplisten_" + inter.getName());
        t.setDaemon(true);
        t.start();
    }

    /**
     * Handles one NetworkInterface.
     * Joins the multicast groups for IPv4, IPv6 or both.
     */
    private class InterfaceHandler {

        /**
         * The MulticastSocket.
         */
        private final MulticastSocket socket;
        /**
         * Use IPv4 for this NetworkInterface.
         */
        private final boolean useIPv4;
        /**
         * Use IPv6 for this NetworkInterface.
         */
        private final boolean useIPv6;

        /**
         * Create a new Handler and set up the MulticastSocket.
         *
         * @param interf the target interface.
         * @throws IOException when there is trouble creating the socket or joining groups
         */
        private InterfaceHandler(final NetworkInterface interf) throws IOException {
            socket = new MulticastSocket(27716);
            // figure out if this interface supports IPv4, IPv6 or both
            Enumeration<InetAddress> addresses = interf.getInetAddresses();
            boolean v4 = false, v6 = false;
            while (addresses.hasMoreElements()) {
                InetAddress address = addresses.nextElement();
                if (address instanceof Inet4Address) {
                    v4 = true;
                } else if (address instanceof Inet6Address) {
                    v6 = true;
                }
            }
            useIPv4 = v4;
            useIPv6 = v6;
            // Configure interface. Not required for IPv6-only interfaces because of the zone index
            if (useIPv4) {
                socket.setNetworkInterface(interf);
                socket.joinGroup(InetAddress.getByName("225.4.5.6"));
            }
            if (useIPv6) {
                socket.joinGroup(Inet6Address.getByAddress("ff15::4c61:6e58:6368:616e:6765", new byte[]{(byte) 0xff, (byte) 0x15, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x4c, (byte) 0x61, (byte) 0x6e, (byte) 0x58, (byte) 0x63, (byte) 0x68, (byte) 0x61, (byte) 0x6e, (byte) 0x67, (byte) 0x65}, interf));
            }
            logger.info("Listening to " + interf.getName() + ", which supports IPv4:" + useIPv4 + " IPv6:" + useIPv6);
        }

        /**
         * Blocks, receives request in a endless loop.
         *
         * @throws IOException on reception errors
         */
        private void handle(final NetworkInterface interf) throws IOException {
            // don't do anything when no supported protocol was found
            if (!useIPv4 && !useIPv6) {
                logger.warn(interf.getName() + " does not seem to support any known protocol!");
                return;
            }
            byte[] buffer = new byte[5];
            DatagramPacket packet = new DatagramPacket(buffer, 5);
            while (true) {
                socket.receive(packet); // Blocks
                // got ping/heartbeat
                listener.pingReceived(packet.getData(), packet.getAddress());
            }
        }

        /**
         * Closes the socket.
         */
        private void close() {
            socket.close();
        }

    }
} public interface PingServerListener {

    /**
     * Called, when a ping is received.
     *
     * @param data the data contained
     * @param host the origin of this ping
     */
    public void pingReceived(byte[] data, InetAddress host);
} public interface Platform {

    /**
     * Weather this platform supports automatic updates.
     *
     * @return true, if supported
     */
    public boolean hasAutoUpdates();

    /**
     * Runs the platform-specific post-update code, if any.
     * This is different from the update itself, as this runs
     * after the restart, so it already uses new code and can patch things up.
     *
     * @throws UnsupportedOperationException if hasAutoUpdate() == false
     * @param args LXC's arguments
     */
    public void postUpdateStep(String[] args);

    /**
     * Triggers the platform-specific update system, if any.
     *
     * @throws UnsupportedOperationException if hasAutoUpdate() == false
     * @param args LXC's arguments
     */
    public void checkAndPerformUpdates(String[] args);

    /**
     * Returns the platform-specific user interface.
     *
     * @param args the start-parameters
     * @return the platform-specific user interface
     */
    public GuiInterface getGui(String[] args);

    /**
     * Reads and returns the configuration.
     *
     * @param args
     */
    public void readConfiguration(String[] args);

    /**
     * Writes the configuration.
     */
    public void writeConfiguration();

    /**
     * Weather this platform supports individually set download targets.
     * True implies that the optional method GuiInterface.getFileTarget() is implemented.
     * There may be a default behaviour weather LanXchange should ask for every download or use a default target most of the time.
     * getDefaultDownloadTarget should return a valid path regardless if asking is supported or not.
     * If getDefaultDownloadTarget does not return a valid path, LXC will ask for a target on each download.
     *
     * @return true, if supported.
     */
    public boolean askForDownloadTargetSupported();

    /**
     * Returns the default download target for new downloads.
     * If only one download target is supported, this path is returned.
     * If multiple paths are supported, but there is a default, the default must be returned.
     * If this returns null, "ask-always" is assumed.
     * Returning null when askForDownloadTargetSupported returned false is forbidden.
     *
     * @return see above
     */
    public String getDefaultDownloadTarget();

    /**
     * Informs about finished downloads.
     * Some platforms (like android) need to be informed about this so they can index the new files.
     */
    public void downloadComplete(LXCFile file, File targetFolder);

    /**
     * Returns ids of MulticastHelpers that are required to
     * make discovery work reliably on this platform.
     *
     * @return all required MulticastHelpers
     */
    public String[] getRequiredMulticastHelpers();
} public interface ProgressIndicator {
    /**
     * This method is called by running transfers to indicate progess.
     * This method can be called very frequently.
     * 
     * @param percentage progress in percent
     */
    public void update(int percentage);

}
import de.tobifleig.lxc.data.FileManager;
import de.tobifleig.lxc.data.LXCFile;
import de.tobifleig.lxc.data.LXCJob;
import de.tobifleig.lxc.log.LXCLogBackend;
import de.tobifleig.lxc.log.LXCLogger;
import de.tobifleig.lxc.net.io.Leecher;
import de.tobifleig.lxc.net.io.Seeder;
import de.tobifleig.lxc.net.io.Transceiver;
import de.tobifleig.lxc.net.io.TransceiverListener;
import de.tobifleig.lxc.net.serv.FileServer;
import de.tobifleig.lxc.net.serv.FileServerListener;
import de.tobifleig.lxc.net.serv.ListServer;
import de.tobifleig.lxc.net.serv.ListServerListener;
import de.tobifleig.lxc.net.serv.PingServer;
import de.tobifleig.lxc.net.serv.PingServerListener;
import de.tobifleig.lxc.plaf.Platform;
import de.tobifleig.lxc.plaf.ProgressIndicator;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.BindException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.NoRouteToHostException;
import java.net.Socket;
import java.util.ConcurrentModificationException;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
/*
 * Copyright 2009, 2010, 2011, 2012, 2013, 2014 Tobias Fleig (tobifleig gmail com)
 *
 * All rights reserved.
 *
 * This file is part of LanXchange.
 *
 * LanXchange 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.
 *
 * LanXchange 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 LanXchange. If not, see <http://www.gnu.org/licenses/>.
 */
package de.tobifleig.lxc.net;


/**
 * Manages networking.
 * Manages (auto-detected) instances, incoming lists/requests.
 *
 * @author Tobias Fleig <tobifleig googlemail com>
 */
public class NetworkManager {

    private final LXCLogger logger;
    /**
     * this server listens for incoming file-requests.
     */
    private final FileServer fileServer;
    /**
     * this server listens for incoming file-lists.
     */
    private final ListServer listServer;
    /**
     * this server listens for ping/keepalives sent by other instances.
     */
    private final PingServer pingServer;
    /**
     * Manages and performs multicasts.
     */
    private final HeartbeatSender multicaster;
    /**
     * Manages NetworkInterfaces.
     * Triggers required actions if an interface goes online or offline
     */
    private final InterfaceManager interfacesurveillance;
    /**
     * Manages remote LXCInstances.
     */
    private final InstanceManager instances;
    /**
     * Used to call LXC when certain events occur.
     */
    private final NetworkManagerListener listener;
    /**
     * Contains all the running jobs.
     */
    private final HashMap<Transceiver, LXCJob> jobs;
    /**
     * Manages all local/remote files.
     */
    private final FileManager fileManager;

    /**
     * creates a new NetworkManager with the given parameters.
     *
     * @param listener the listener to pass events to
     * @param fileManager the filemanager who must be informed about received filelists etc.
     * @param platform the platform we are running on
     */
    public NetworkManager(final NetworkManagerListener listener, FileManager fileManager, Platform platform) {
        this.logger = LXCLogBackend.getLogger("network-manager");
        this.listener = listener;
        this.fileManager = fileManager;
        jobs = new HashMap<Transceiver, LXCJob>();
        instances = new InstanceManager(new InstanceManagerListener() {
            @Override
            public void instanceAdded(LXCInstance newInstance) {
                broadcastList();
                // request list
                sendList(null, newInstance);
            }

            @Override
            public void instanceRemoved(LXCInstance removedInstance) {
                NetworkManager.this.listener.instanceRemoved(removedInstance);
            }
        });
        fileServer = new FileServer(new FileServerListener() {
            @Override
            public void downloadRequest(Socket socket, final LXCFile file, ObjectOutputStream outStream, ObjectInputStream inStream, InetAddress address, int transVersion) {
                final Seeder seed = new Seeder(socket, outStream, inStream, file, transVersion);
                TransceiverListener seedListener = new TransceiverListener() {
                    @Override
                    public void finished(boolean success, boolean cancelled, boolean removeFile, String problemFilePath) {
                        int removeIndex = file.getJobs().indexOf(jobs.get(seed));
                        listener.notifyRemoveJob(file, removeIndex);
                        file.removeJob(jobs.get(seed), success);
                        if (removeFile) {
                            // File no longer available --> remove
                            NetworkManager.this.fileManager.removeLocal(file);
                            broadcastList();
                            NetworkManager.this.listener.uploadFailedFileMissing(file);
                        }
                        jobs.remove(seed);
                        NetworkManager.this.listener.refreshGui();
                    }
                };
                seed.setListener(seedListener);
                seed.setProgressIndicator(new ProgressIndicator() {

                    @Override
                    public void update(int percentage) {
                        NetworkManager.this.listener.refreshGui();
                    }
                });

                LXCJob job = new LXCJob(seed, instances.searchByAddress(address));
                jobs.put(seed, job);
                file.addJob(job);
                listener.notifyJobAdded(file, file.getJobs().size() - 1);

                seed.start();
            }
        }, fileManager);
        listServer = new ListServer(new ListServerListener() {