package org.limewire.ui.swing.upload; import java.awt.BorderLayout; import java.awt.event.ActionEvent; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.List; import javax.swing.AbstractAction; import javax.swing.BorderFactory; import javax.swing.JButton; import javax.swing.JComponent; import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JPopupMenu; import javax.swing.JScrollPane; import org.limewire.bittorrent.TorrentManager; import org.limewire.collection.glazedlists.GlazedListsFactory; import org.limewire.core.api.Category; import org.limewire.core.api.upload.UploadItem; import org.limewire.core.api.upload.UploadListManager; import org.limewire.core.api.upload.UploadState; import org.limewire.core.api.upload.UploadItem.UploadItemType; import org.limewire.core.settings.SharingSettings; import org.limewire.inject.EagerSingleton; import org.limewire.lifecycle.ServiceRegistry; import org.limewire.setting.evt.SettingEvent; import org.limewire.setting.evt.SettingListener; import org.limewire.ui.swing.components.FocusJOptionPane; import org.limewire.ui.swing.components.HyperlinkButton; import org.limewire.ui.swing.components.MultiLineLabel; import org.limewire.ui.swing.settings.SwingUiSettings; import org.limewire.ui.swing.transfer.TransferTrayNavigator; import org.limewire.ui.swing.upload.table.UploadTable; import org.limewire.ui.swing.upload.table.UploadTableFactory; import org.limewire.ui.swing.util.GuiUtils; import org.limewire.ui.swing.util.I18n; import org.limewire.ui.swing.util.SwingUtils; import org.limewire.util.FileUtils; import org.limewire.util.Objects; import ca.odell.glazedlists.EventList; import ca.odell.glazedlists.SortedList; import ca.odell.glazedlists.event.ListEvent; import ca.odell.glazedlists.event.ListEventListener; import ca.odell.glazedlists.matchers.Matcher; import com.google.inject.Inject; import com.google.inject.Provider; /** * Mediator to control the interaction between the uploads table and various * services. */ @EagerSingleton public class UploadMediator { public enum SortOrder { ORDER_STARTED, NAME, PROGRESS, TIME_REMAINING, SPEED, STATUS, FILE_TYPE, FILE_EXTENSION, USER_NAME } public static final String NAME = "UploadPanel"; private final UploadListManager uploadListManager; private final Provider<TorrentManager> torrentManager; private final UploadTableFactory uploadTableFactory; private final Provider<TransferTrayNavigator> transferTrayNavigator; private EventList<UploadItem> activeList; private SortedList<UploadItem> sortedList; private JPanel uploadPanel; private UploadTable uploadTable; private JButton clearFinishedButton; private List<JButton> headerButtons; private JPopupMenu headerPopupMenu; @Inject public UploadMediator(UploadListManager uploadListManager, Provider<TorrentManager> torrentManager, UploadTableFactory uploadTableFactory, Provider<TransferTrayNavigator> transferTrayNavigator) { this.uploadListManager = uploadListManager; this.torrentManager = torrentManager; this.uploadTableFactory = uploadTableFactory; this.transferTrayNavigator = transferTrayNavigator; sortedList = GlazedListsFactory.sortedList(uploadListManager.getSwingThreadSafeUploads(), new OrderedComparator<UploadItem>(getSortComparator(getSortOrder()), isSortAscending())); } /** * Start the (polling) upload monitor. * <p> * Note: this only makes sense if this component is created on demand. */ @Inject public void register(ServiceRegistry serviceRegister) { serviceRegister.start(uploadListManager); // Add setting listener to clear finished uploads. When set, we clear // finished uploads and hide the "clear finished" button. SharingSettings.CLEAR_UPLOAD.addSettingListener(new SettingListener() { @Override public void settingChanged(SettingEvent evt) { SwingUtils.invokeNowOrLater(new Runnable() { @Override public void run() { boolean clearUploads = SharingSettings.CLEAR_UPLOAD.getValue(); if (clearUploads) { clearFinished(); } if (clearFinishedButton != null) { clearFinishedButton.setVisible(!clearUploads); } } }); } }); // Add list listener to enable "clear finished" button. EventList<UploadItem> doneList = GlazedListsFactory.filterList( uploadListManager.getSwingThreadSafeUploads(), new CompleteUploadMatcher()); doneList.addListEventListener(new ListEventListener<UploadItem>() { @Override public void listChanged(ListEvent<UploadItem> listChanges) { if (clearFinishedButton != null) { clearFinishedButton.setEnabled(listChanges.getSourceList().size() > 0); } } }); } /** * Returns the component of this mediator. */ public JComponent getComponent() { if (uploadPanel == null) { uploadPanel = createUploadPanel(); } return uploadPanel; } /** * Creates a display panel containing the upload table. */ private JPanel createUploadPanel() { JPanel panel = new JPanel(new BorderLayout()); uploadTable = uploadTableFactory.create(this); uploadTable.setTableHeader(null); JScrollPane scrollPane = new JScrollPane(uploadTable); scrollPane.setBorder(BorderFactory.createEmptyBorder()); panel.add(scrollPane, BorderLayout.CENTER); return panel; } /** * Returns a list of active upload items. */ public EventList<UploadItem> getActiveList() { if (activeList == null) { activeList = GlazedListsFactory.filterList( uploadListManager.getSwingThreadSafeUploads(), new ActiveUploadMatcher()); } return activeList; } /** * Returns a list of header buttons. */ public List<JButton> getHeaderButtons() { if (headerButtons == null) { clearFinishedButton = new HyperlinkButton(new ClearFinishedAction()); clearFinishedButton.setVisible(!SharingSettings.CLEAR_UPLOAD.getValue()); headerButtons = new ArrayList<JButton>(); headerButtons.add(clearFinishedButton); } return headerButtons; } /** * Returns the header popup menu associated with the uploads table. */ public JPopupMenu getHeaderPopupMenu() { if (headerPopupMenu == null) { headerPopupMenu = new UploadHeaderPopupMenu(this, torrentManager, transferTrayNavigator); } return headerPopupMenu; } /** * Returns a sorted list of uploads. */ public EventList<UploadItem> getUploadList() { return sortedList; } /** * Returns a list of selected upload items. */ public List<UploadItem> getSelectedUploads() { if (uploadTable != null) { return uploadTable.getSelectedItems(); } else { return Collections.emptyList(); } } /** * Returns true if the uploads list is sorted in ascending order. */ public boolean isSortAscending() { return SwingUiSettings.UPLOAD_SORT_ASCENDING.getValue(); } /** * Returns the sort key for the uploads list. */ public SortOrder getSortOrder() { try { String sortKey = SwingUiSettings.UPLOAD_SORT_KEY.get(); return SortOrder.valueOf(sortKey); } catch (IllegalArgumentException ex) { // Return default order if setting is invalid. return SortOrder.ORDER_STARTED; } } /** * Sets the sort key and direction on the uploads list. */ public void setSortOrder(SortOrder sortOrder, boolean ascending) { // Save sort settings. SwingUiSettings.UPLOAD_SORT_KEY.set(sortOrder.toString()); SwingUiSettings.UPLOAD_SORT_ASCENDING.setValue(ascending); // Apply sort order. sortedList.setComparator(new OrderedComparator<UploadItem>( getSortComparator(sortOrder), ascending)); } /** * Returns a comparator for the specified sort key and direction. */ private Comparator<UploadItem> getSortComparator(SortOrder sortOrder) { switch (sortOrder) { case ORDER_STARTED: return new OrderStartedComparator(); case NAME: return new NameComparator(); case PROGRESS: return new ProgressComparator(); case TIME_REMAINING: return new TimeRemainingComparator(); case SPEED: return new SpeedComparator(); case STATUS: return new StateComparator(); case FILE_TYPE: return new CategoryComparator(); case FILE_EXTENSION: return new FileExtensionComparator(); case USER_NAME: return new HostNameComparator(); default: throw new IllegalArgumentException("Unknown SortOrder: " + sortOrder); } } /** * Returns true if any uploads may be paused. */ public boolean hasPausable() { EventList<UploadItem> uploadList = getUploadList(); uploadList.getReadWriteLock().readLock().lock(); try { for (UploadItem item : uploadList) { if (isPausable(item)) return true; } } finally { uploadList.getReadWriteLock().readLock().unlock(); } return false; } /** * Returns true if any uploads may be resumed. */ public boolean hasResumable() { EventList<UploadItem> uploadList = getUploadList(); uploadList.getReadWriteLock().readLock().lock(); try { for (UploadItem item : uploadList) { if (isResumable(item)) return true; } } finally { uploadList.getReadWriteLock().readLock().unlock(); } return false; } /** * Returns true if any uploads are in an error state. */ public boolean hasErrors() { EventList<UploadItem> uploadList = getUploadList(); uploadList.getReadWriteLock().readLock().lock(); try { for (UploadItem item : uploadList) { if (item.getState().isError()) return true; } } finally { uploadList.getReadWriteLock().readLock().unlock(); } return false; } /** * Returns true if any uploads are torrents. */ public boolean hasTorrents() { EventList<UploadItem> uploadList = getUploadList(); uploadList.getReadWriteLock().readLock().lock(); try { for (UploadItem item : uploadList) { if (item.getUploadItemType() == UploadItemType.BITTORRENT) return true; } } finally { uploadList.getReadWriteLock().readLock().unlock(); } return false; } /** * Returns true if the specified upload item is a browse item. */ public static boolean isBrowseHost(UploadItem uploadItem) { UploadState state = uploadItem.getState(); return (state == UploadState.BROWSE_HOST) || (state == UploadState.BROWSE_HOST_DONE); } /** * Returns true if the specified upload item may be paused. */ public static boolean isPausable(UploadItem uploadItem) { return (uploadItem.getUploadItemType() == UploadItemType.BITTORRENT) && (uploadItem.getState() == UploadState.UPLOADING); } /** * Returns true if the specified upload item may be resumed. */ public static boolean isResumable(UploadItem uploadItem) { return (uploadItem.getUploadItemType() == UploadItemType.BITTORRENT) && (uploadItem.getState() == UploadState.PAUSED); } /** * Returns true if the specified upload item may be removed. */ public static boolean isRemovable(UploadItem uploadItem) { UploadState state = uploadItem.getState(); return state.isFinished() || state.isError(); } /** * Cancels the specified upload item. The method prompts the user to * cancel torrent uploads. If <code>remove</code> is true, the cancelled * item is also removed from the list. */ public void cancel(UploadItem uploadItem, boolean remove) { cancel(uploadItem, remove, true); } /** * Cancels the specified upload item. If <code>prompt</code> is true the method * prompts the user to cancel torrent uploads. If <code>remove</code> is true, the * cancelled item is also removed from the list. */ public void cancel(UploadItem uploadItem, boolean remove, boolean prompt) { boolean approved = true; // For torrents, determine cancel approval based on torrent status and // user prompt. There are various reasons the user will not want the // cancel to go through. // 1) If the torrent is still downloading, the upload cannot be cancelled // without cancelling the download. // 2) If the torrent is seeding, but the seed ratio is low, the user may // wish to seed to at least 100% to be a good samaritan. if (uploadItem.getUploadItemType() == UploadItemType.BITTORRENT) { if (!uploadItem.isStarted()) { approved = false; } else if (prompt && !uploadItem.isFinished()) { approved = promptUser(I18n.tr("If you stop this upload, the torrent download will stop. Are you sure you want to do this?")); } else if (prompt && uploadItem.getSeedRatio() < 1.0f) { approved = promptUser(I18n.tr("Are you sure you want to stop this upload?")); } } // Cancel upload if approved, and remove from list if specified. if (approved) { uploadItem.cancel(); if (remove) { remove(uploadItem); } } } /** * Removes the specified upload item from the upload list. */ public void remove(UploadItem uploadItem) { uploadListManager.remove(uploadItem); } /** * Displays a Yes/No prompt to the user with the specified message, and * returns true if the user presses Yes. */ private boolean promptUser(String message) { return FocusJOptionPane.showConfirmDialog(GuiUtils.getMainFrame(), new MultiLineLabel(message, 400), I18n.tr("Uploads"), JOptionPane.YES_NO_OPTION) == JOptionPane.YES_OPTION; } /** * Cancels all uploads. */ public void cancelAll() { List<UploadItem> uploadList = new ArrayList<UploadItem>(getUploadList()); for (UploadItem item : uploadList) { cancel(item, false, false); } } /** * Cancels all uploads in an error state. */ public void cancelAllError() { List<UploadItem> uploadList = new ArrayList<UploadItem>(getUploadList()); for (UploadItem item : uploadList) { if (item.getState().isError()) cancel(item, false, false); } } /** * Cancels all torrent uploads. */ public void cancelAllTorrents() { List<UploadItem> uploadList = new ArrayList<UploadItem>(getUploadList()); for (UploadItem item : uploadList) { if (item.getUploadItemType() == UploadItemType.BITTORRENT) cancel(item, false, false); } } /** * Clears all finished uploads. */ private void clearFinished() { uploadListManager.clearFinished(); } /** * Pauses all uploads that can be paused. */ public void pauseAll() { EventList<UploadItem> uploadList = getUploadList(); uploadList.getReadWriteLock().readLock().lock(); try { for (UploadItem item : uploadList) { if (isPausable(item)) item.pause(); } } finally { uploadList.getReadWriteLock().readLock().unlock(); } } /** * Resumes all uploads that can be resumed. */ public void resumeAll() { EventList<UploadItem> uploadList = getUploadList(); uploadList.getReadWriteLock().readLock().lock(); try { for (UploadItem item : uploadList) { if (isResumable(item)) item.resume(); } } finally { uploadList.getReadWriteLock().readLock().unlock(); } } /** * Action to clear all finished uploads. */ private class ClearFinishedAction extends AbstractAction { public ClearFinishedAction() { super(I18n.tr("Clear Finished")); } @Override public void actionPerformed(ActionEvent e) { clearFinished(); } } /** * Returns true if the UploadItem is currently active, false otherwise. */ private class ActiveUploadMatcher implements Matcher<UploadItem> { @Override public boolean matches(UploadItem item) { if (item == null) return false; UploadState state = item.getState(); return state == UploadState.QUEUED || state == UploadState.UPLOADING; } } /** * Return true if the UploadItem is in a compelte state, false otherwise. */ private class CompleteUploadMatcher implements Matcher<UploadItem> { @Override public boolean matches(UploadItem item) { if (item == null) return false; UploadState state = item.getState(); return state == UploadState.DONE || state == UploadState.LIMIT_REACHED || state == UploadState.CANCELED || state == UploadState.BROWSE_HOST || state == UploadState.BROWSE_HOST_DONE || state == UploadState.REQUEST_ERROR; } } private static class OrderStartedComparator implements Comparator<UploadItem> { @Override public int compare(UploadItem o1, UploadItem o2) { if (o1 == o2) return 0; return (int) (o1.getStartTime() - o2.getStartTime()); } } private static class NameComparator implements Comparator<UploadItem> { @Override public int compare(UploadItem o1, UploadItem o2) { if (o1 == o2) return 0; String name1 = o1.getFileName(); String name2 = o2.getFileName(); return Objects.compareToNullIgnoreCase(name1, name2, false); } } private static class ProgressComparator implements Comparator<UploadItem> { @Override public int compare(UploadItem o1, UploadItem o2) { if (o1 == o2) return 0; int pct1 = getProgressPct(o1); int pct2 = getProgressPct(o2); return (pct1 - pct2); } private int getProgressPct(UploadItem item) { // browses have no file size so sort them together below file uploads if(item.getFileSize() <= 0) return -1; return (int) (100 * item.getTotalAmountUploaded() / item.getFileSize()); } } private static class TimeRemainingComparator implements Comparator<UploadItem> { @Override public int compare(UploadItem o1, UploadItem o2) { if (o1 == o2) return 0; long time1 = o1.getRemainingUploadTime(); long time2 = o2.getRemainingUploadTime(); return (int) (time1 - time2); } } private static class SpeedComparator implements Comparator<UploadItem> { @Override public int compare(UploadItem o1, UploadItem o2) { if (o1 == o2) return 0; float speed1 = o1.getUploadSpeed(); float speed2 = o2.getUploadSpeed(); return (int) (speed1 - speed2); } } private static class StateComparator implements Comparator<UploadItem> { @Override public int compare(UploadItem o1, UploadItem o2) { if (o1 == o2) return 0; int value1 = getSortValue(o1.getState()); int value2 = getSortValue(o2.getState()); return (value1 - value2); } private int getSortValue(UploadState state) { switch (state) { case DONE: return 1; case UPLOADING: return 2; case PAUSED: return 3; case QUEUED: return 4; case REQUEST_ERROR: return 5; case LIMIT_REACHED: return 5; case CANCELED: return 6; case BROWSE_HOST: return 7; case BROWSE_HOST_DONE: return 8; default: throw new IllegalArgumentException("Unknown UploadState: " + state); } } } private static class CategoryComparator implements Comparator<UploadItem> { @Override public int compare(UploadItem o1, UploadItem o2) { if (o1 == o2) return 0; Category cat1 = o1.getCategory(); Category cat2 = o2.getCategory(); return cat1.compareTo(cat2); } } private static class FileExtensionComparator implements Comparator<UploadItem> { @Override public int compare(UploadItem o1, UploadItem o2) { if (o1 == o2) return 0; String name1 = o1.getFileName(); String name2 = o2.getFileName(); if (name1 == null) { return (name2 == null) ? 0 : -1; } else if (name2 == null) { return 1; } String ext1 = FileUtils.getFileExtension(name1); String ext2 = FileUtils.getFileExtension(name2); return Objects.compareToNullIgnoreCase(ext1, ext2, false); } } private static class HostNameComparator implements Comparator<UploadItem> { @Override public int compare(UploadItem o1, UploadItem o2) { if (o1 == o2) return 0; String name1 = o1.getRenderName(); String name2 = o2.getRenderName(); return Objects.compareToNullIgnoreCase(name1, name2, false); } } private static class OrderedComparator<T> implements Comparator<T> { private final Comparator<T> delegate; private final boolean ascending; public OrderedComparator(Comparator<T> delegate, boolean ascending) { this.delegate = delegate; this.ascending = ascending; } @Override public int compare(T o1, T o2) { return (ascending ? 1 : -1) * delegate.compare(o1, o2); } } }