org.syncany.gui.tray.TrayIcon.java Source code

Java tutorial

Introduction

Here is the source code for org.syncany.gui.tray.TrayIcon.java

Source

/*
 * Syncany, www.syncany.org
 * Copyright (C) 2011-2013 Philipp C. Heckel <philipp.heckel@gmail.com> 
 *
 * 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 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package org.syncany.gui.tray;

import static org.syncany.gui.util.I18n._;

import java.io.File;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Shell;
import org.syncany.config.GuiEventBus;
import org.syncany.gui.util.DesktopUtil;
import org.syncany.gui.wizard.WizardDialog;
import org.syncany.operations.ChangeSet;
import org.syncany.operations.daemon.Watch;
import org.syncany.operations.daemon.Watch.SyncStatus;
import org.syncany.operations.daemon.messages.DaemonReloadedExternalEvent;
import org.syncany.operations.daemon.messages.DownChangesDetectedSyncExternalEvent;
import org.syncany.operations.daemon.messages.DownDownloadFileSyncExternalEvent;
import org.syncany.operations.daemon.messages.DownEndSyncExternalEvent;
import org.syncany.operations.daemon.messages.DownStartSyncExternalEvent;
import org.syncany.operations.daemon.messages.ExitGuiInternalEvent;
import org.syncany.operations.daemon.messages.ListWatchesManagementRequest;
import org.syncany.operations.daemon.messages.ListWatchesManagementResponse;
import org.syncany.operations.daemon.messages.UpEndSyncExternalEvent;
import org.syncany.operations.daemon.messages.UpIndexChangesDetectedSyncExternalEvent;
import org.syncany.operations.daemon.messages.UpIndexStartSyncExternalEvent;
import org.syncany.operations.daemon.messages.UpStartSyncExternalEvent;
import org.syncany.operations.daemon.messages.UpUploadFileInTransactionSyncExternalEvent;
import org.syncany.operations.daemon.messages.UpUploadFileSyncExternalEvent;
import org.syncany.operations.daemon.messages.WatchEndSyncExternalEvent;
import org.syncany.util.FileUtil;
import org.syncany.util.StringUtil;

import com.google.common.base.Predicate;
import com.google.common.collect.Maps;
import com.google.common.eventbus.Subscribe;

/**
 * Represents the tray icon, showing the status of the application,
 * a menu to control the application and the ability to display
 * notifications. The tray icon is the central entry point for
 * the application.
 * 
 * @author Philipp C. Heckel <philipp.heckel@gmail.com>
 * @author Vincent Wiencek <vwiencek@gmail.com>
 */
public abstract class TrayIcon {
    private static final Logger logger = Logger.getLogger(TrayIcon.class.getSimpleName());

    private static int REFRESH_TIME = 500;
    private static String URL_REPORT_ISSUE = "https://www.syncany.org/r/issue";
    private static String URL_DONATE = "https://www.syncany.org/donate.html";
    private static String URL_HOMEPAGE = "https://www.syncany.org";

    protected Shell trayShell;
    protected WizardDialog wizard;
    protected GuiEventBus eventBus;
    protected Map<String, String> messages;

    private Thread systemTrayAnimationThread;
    private AtomicBoolean syncing;
    private Map<String, Boolean> clientSyncStatus;
    private Map<String, Long> clientUploadFileSize;

    public TrayIcon(Shell shell) {
        this.trayShell = shell;
        this.messages = new HashMap<String, String>();

        this.eventBus = GuiEventBus.getInstance();
        this.eventBus.register(this);

        this.syncing = new AtomicBoolean(false);
        this.clientSyncStatus = Maps.newConcurrentMap();
        this.clientUploadFileSize = Maps.newConcurrentMap();

        initInternationalization();
        initAnimationThread();
        initTrayImage();
    }

    protected void showNew() {
        Display.getDefault().asyncExec(new Runnable() {
            @Override
            public void run() {
                if (wizard == null) {
                    wizard = new WizardDialog(trayShell);
                    wizard.open();

                    wizard = null;
                }
            }
        });
    }

    protected void showFolder(File folder) {
        DesktopUtil.launch(folder.getAbsolutePath());
    }

    protected void showReportIssue() {
        DesktopUtil.launch(URL_REPORT_ISSUE);
    }

    protected void showDonate() {
        DesktopUtil.launch(URL_DONATE);
    }

    protected void showWebsite() {
        DesktopUtil.launch(URL_HOMEPAGE);
    }

    protected void exitApplication() {
        dispose();
        eventBus.post(new ExitGuiInternalEvent());
    }

    @Subscribe
    public void onDaemonReloadedEventReceived(DaemonReloadedExternalEvent daemonReloadedEvent) {
        eventBus.post(new ListWatchesManagementRequest());
    }

    @Subscribe
    public void onListWatchesResponseReceived(ListWatchesManagementResponse listWatchesResponse) {
        logger.log(Level.FINE,
                "List watches response recevied: " + listWatchesResponse.getWatches().size() + " watch(es)");

        cleanSyncStatus();
        List<File> watchedFolders = new ArrayList<File>();

        for (Watch watch : listWatchesResponse.getWatches()) {
            watchedFolders.add(watch.getFolder());

            boolean watchFolderIsSyncing = watch.getStatus() == SyncStatus.SYNCING;
            updateSyncStatus(watch.getFolder().getAbsolutePath(), watchFolderIsSyncing);
        }

        // Update folders in menu
        setWatchedFolders(watchedFolders);

        // Update tray icon
        if (!syncing.get()) {
            setTrayImage(TrayIconImage.TRAY_IN_SYNC);
            logger.log(Level.FINE, "Syncing image: Setting to image " + TrayIconImage.TRAY_IN_SYNC);
        }
    }

    @Subscribe
    public void onDownChangesDetectedEvent(DownChangesDetectedSyncExternalEvent downChangesDetectedEvent) {
        updateSyncStatus(downChangesDetectedEvent.getRoot(), true);
    }

    @Subscribe
    public void onUpIndexChangesDetectedEvent(UpIndexChangesDetectedSyncExternalEvent upIndexChangesDetectedEvent) {
        updateSyncStatus(upIndexChangesDetectedEvent.getRoot(), true);
    }

    @Subscribe
    public void onWatchEndEventReceived(WatchEndSyncExternalEvent watchEndEvent) {
        updateSyncStatus(watchEndEvent.getRoot(), false);
    }

    @Subscribe
    public void onUpStartEventReceived(UpStartSyncExternalEvent syncEvent) {
        setStatusText(syncEvent.getRoot(), _("org.syncany.gui.tray.TrayIcon.up.start"));
    }

    @Subscribe
    public void onIndexStartEventReceived(UpIndexStartSyncExternalEvent syncEvent) {
        if (syncEvent.getFileCount() > 0) {
            setStatusText(syncEvent.getRoot(),
                    _("org.syncany.gui.tray.TrayIcon.up.indexStartWithFileCount", syncEvent.getFileCount()));
        } else {
            setStatusText(syncEvent.getRoot(), _("org.syncany.gui.tray.TrayIcon.up.indexStartWithoutFileCount"));
        }
    }

    @Subscribe
    public void onUploadFileEventReceived(UpUploadFileSyncExternalEvent syncEvent) {
        if (syncEvent.getFilename() != null) {
            setStatusText(syncEvent.getRoot(),
                    _("org.syncany.gui.tray.TrayIcon.up.uploadWithFilename", syncEvent.getFilename()));
        } else {
            setStatusText(syncEvent.getRoot(), _("org.syncany.gui.tray.TrayIcon.up.uploadWithoutFilename"));
        }
    }

    @Subscribe
    public void onUploadFileInTransactionEventReceived(UpUploadFileInTransactionSyncExternalEvent syncEvent) {
        Long currentClientUploadFileSize = clientUploadFileSize.get(syncEvent.getRoot());

        if (currentClientUploadFileSize == null || syncEvent.getCurrentFileIndex() <= 1) {
            currentClientUploadFileSize = 0L;
        }

        String uploadedTotalStr = FileUtil.formatFileSize(currentClientUploadFileSize);
        int uploadedPercent = (int) Math
                .round((double) currentClientUploadFileSize / syncEvent.getTotalFileSize() * 100);

        String statusText = _("org.syncany.gui.tray.TrayIcon.up.uploadFileInTransaction",
                syncEvent.getCurrentFileIndex(), syncEvent.getTotalFileCount(), uploadedTotalStr, uploadedPercent);
        setStatusText(syncEvent.getRoot(), statusText);

        currentClientUploadFileSize += syncEvent.getCurrentFileSize();
        clientUploadFileSize.put(syncEvent.getRoot(), currentClientUploadFileSize);
    }

    @Subscribe
    public void onUpEndEventReceived(UpEndSyncExternalEvent syncEvent) {
        setStatusText(syncEvent.getRoot(), _("tray.menuitem.status.insync"));
    }

    @Subscribe
    public void onDownDownloadFileSyncEventReceived(DownDownloadFileSyncExternalEvent syncEvent) {
        String fileDescription = syncEvent.getFileDescription();
        int currentFileIndex = syncEvent.getCurrentFileIndex();
        int maxFileCount = syncEvent.getMaxFileCount();

        setStatusText(syncEvent.getRoot(), _("org.syncany.gui.tray.TrayIcon.down.downloadFile", fileDescription,
                currentFileIndex, maxFileCount));
    }

    @Subscribe
    public void onDownStartEventReceived(DownStartSyncExternalEvent syncEvent) {
        setStatusText(syncEvent.getRoot(), _("org.syncany.gui.tray.TrayIcon.down.start"));
    }

    @Subscribe
    public void onDownEndEventReceived(DownEndSyncExternalEvent downEndSyncEvent) {
        // Display notification
        ChangeSet changeSet = downEndSyncEvent.getChanges();

        if (changeSet.hasChanges()) {
            String rootName = new File(downEndSyncEvent.getRoot()).getName();
            int totalChangedFiles = changeSet.getNewFiles().size() + changeSet.getChangedFiles().size()
                    + changeSet.getDeletedFiles().size();

            String subject = "";
            String message = "";

            if (totalChangedFiles == 1) {
                if (changeSet.getNewFiles().size() == 1) {
                    subject = _("org.syncany.gui.tray.TrayIcon.notify.added.subject",
                            changeSet.getNewFiles().first());
                    message = _("org.syncany.gui.tray.TrayIcon.notify.added.message",
                            changeSet.getNewFiles().first(), rootName);
                }

                if (changeSet.getChangedFiles().size() == 1) {
                    subject = _("org.syncany.gui.tray.TrayIcon.notify.changed.subject",
                            changeSet.getChangedFiles().first());
                    message = _("org.syncany.gui.tray.TrayIcon.notify.changed.message",
                            changeSet.getChangedFiles().first(), rootName);
                }

                if (changeSet.getDeletedFiles().size() == 1) {
                    subject = _("org.syncany.gui.tray.TrayIcon.notify.deleted.subject",
                            changeSet.getDeletedFiles().first());
                    message = _("org.syncany.gui.tray.TrayIcon.notify.deleted.message",
                            changeSet.getDeletedFiles().first(), rootName);
                }
            } else {
                List<String> messageParts = new ArrayList<>();

                if (changeSet.getNewFiles().size() > 0) {
                    if (changeSet.getNewFiles().size() == 1) {
                        messageParts.add(_("org.syncany.gui.tray.TrayIcon.notify.added.one"));
                    } else {
                        messageParts.add(_("org.syncany.gui.tray.TrayIcon.notify.added.many",
                                changeSet.getNewFiles().size()));
                    }
                }

                if (changeSet.getChangedFiles().size() > 0) {
                    if (changeSet.getChangedFiles().size() == 1) {
                        messageParts.add(_("org.syncany.gui.tray.TrayIcon.notify.changed.one"));
                    } else {
                        messageParts.add(_("org.syncany.gui.tray.TrayIcon.notify.changed.many",
                                changeSet.getChangedFiles().size()));
                    }
                }

                if (changeSet.getDeletedFiles().size() > 0) {
                    if (changeSet.getDeletedFiles().size() == 1) {
                        messageParts.add(_("org.syncany.gui.tray.TrayIcon.notify.deleted.one"));
                    } else {
                        messageParts.add(_("org.syncany.gui.tray.TrayIcon.notify.deleted.many",
                                changeSet.getDeletedFiles().size()));
                    }
                }

                subject = _("org.syncany.gui.tray.TrayIcon.notify.synced.subject", rootName);
                message = _("org.syncany.gui.tray.TrayIcon.notify.synced.message",
                        StringUtil.join(messageParts, ", "), rootName);
            }

            displayNotification(subject, message);
        }
    }

    private void initInternationalization() {
        messages.put("tray.menuitem.new", _("tray.menuitem.new"));
        messages.put("tray.menuitem.status.insync", _("tray.menuitem.status.insync"));
        messages.put("tray.menuitem.issue", _("tray.menuitem.issue"));
        messages.put("tray.menuitem.donate", _("tray.menuitem.donate"));
        messages.put("tray.menuitem.exit", _("tray.menuitem.exit"));
        messages.put("tray.menuitem.website", _("tray.menuitem.website"));
    }

    private void initAnimationThread() {
        systemTrayAnimationThread = new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    while (!syncing.get()) {
                        try {
                            Thread.sleep(200);
                        } catch (InterruptedException e) {
                            // Don't care
                        }
                    }

                    int trayImageIndex = 0;

                    while (syncing.get()) {
                        try {
                            TrayIconImage syncImage = TrayIconImage.getSyncImage(trayImageIndex);
                            setTrayImage(syncImage);

                            logger.log(Level.FINE, "Syncing image: Setting image to " + syncImage);

                            trayImageIndex = (trayImageIndex + 1) % TrayIconImage.MAX_SYNC_IMAGES;
                            Thread.sleep(REFRESH_TIME);
                        } catch (InterruptedException e) {
                            // Don't care
                        }
                    }

                    setTrayImage(TrayIconImage.TRAY_IN_SYNC);
                    setStatusText(null, _("tray.menuitem.status.insync"));

                    logger.log(Level.FINE, "Syncing image: Setting image to " + TrayIconImage.TRAY_IN_SYNC);
                }
            }
        });

        systemTrayAnimationThread.start();
    }

    private void initTrayImage() {
        setTrayImage(TrayIconImage.TRAY_NO_OVERLAY);
        logger.log(Level.FINE, "Syncing image: Setting image to " + TrayIconImage.TRAY_NO_OVERLAY);
    }

    private void cleanSyncStatus() {
        logger.log(Level.FINE, "Resetting sync status for clients.");
        clientSyncStatus.clear();
    }

    private void updateSyncStatus(String root, boolean syncStatus) {
        clientSyncStatus.put(root, syncStatus);
        logger.log(Level.FINE, "Sync status for " + root + ": " + syncStatus);

        // Update 'syncing' variable: Set true if any of the folders is syncing
        Map<String, Boolean> syncingFolders = Maps.filterValues(clientSyncStatus, new Predicate<Boolean>() {
            @Override
            public boolean apply(Boolean syncStatus) {
                return syncStatus;
            }
        });

        syncing.set(syncingFolders.size() > 0);
    }

    // Abstract methods

    protected abstract void setTrayImage(TrayIconImage image);

    protected abstract void setWatchedFolders(List<File> folders);

    protected abstract void setStatusText(String root, String statusText);

    protected abstract void displayNotification(String subject, String message);

    protected abstract void dispose();
}