/* GNU GENERAL PUBLIC LICENSE Copyright (C) 2006 The Lobo Project 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 verion 2 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 library; if not, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA Contact info: lobochief@users.sourceforge.net */ package org.lobobrowser.primary.gui.download; import java.awt.Component; import java.awt.Container; import java.awt.Dimension; import java.awt.FlowLayout; import java.awt.event.ActionEvent; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.URL; import java.util.logging.Level; import java.util.logging.Logger; import javax.swing.AbstractAction; import javax.swing.Box; import javax.swing.BoxLayout; import javax.swing.JButton; import javax.swing.JFileChooser; import javax.swing.JFrame; import javax.swing.JOptionPane; import javax.swing.JProgressBar; import javax.swing.border.EmptyBorder; import javax.swing.SwingUtilities; import org.eclipse.jdt.annotation.NonNull; import org.lobobrowser.clientlet.ClientletException; import org.lobobrowser.clientlet.ClientletRequest; import org.lobobrowser.clientlet.ClientletResponse; import org.lobobrowser.gui.DefaultWindowFactory; import org.lobobrowser.primary.gui.FieldType; import org.lobobrowser.primary.gui.FormField; import org.lobobrowser.primary.gui.FormPanel; import org.lobobrowser.primary.settings.ToolsSettings; import org.lobobrowser.request.AbstractRequestHandler; import org.lobobrowser.request.ClientletRequestImpl; import org.lobobrowser.request.RequestEngine; import org.lobobrowser.request.RequestHandler; import org.lobobrowser.ua.ProgressType; import org.lobobrowser.ua.RequestType; import org.lobobrowser.ua.UserAgentContext; import org.lobobrowser.util.OS; import org.lobobrowser.util.Timing; public class DownloadDialog extends JFrame { private static final long serialVersionUID = -733135175100739218L; private static final Logger logger = Logger.getLogger(DownloadDialog.class.getName()); private final JProgressBar progressBar = new JProgressBar(); private final FormPanel bottomFormPanel = new FormPanel(); private final FormPanel topFormPanel = new FormPanel(); private final FormField documentField = new FormField(FieldType.TEXT, false); private final FormField sizeField = new FormField(FieldType.TEXT, false); private final FormField destinationField = new FormField(FieldType.TEXT, false); private final FormField timeLeftField = new FormField(FieldType.TEXT, false); private final FormField mimeTypeField = new FormField(FieldType.TEXT, false); private final FormField transferRateField = new FormField(FieldType.TEXT, false); private final FormField transferSizeField = new FormField(FieldType.TEXT, false); private final JButton saveButton = new JButton(); private final JButton closeButton = new JButton(); private final JButton openFolderButton = new JButton(); private final JButton openButton = new JButton(); private final @NonNull URL url; private final int knownContentLength; public DownloadDialog(final ClientletResponse response, final @NonNull URL url, final int transferSpeed, final UserAgentContext uaContext) { this.url = url; this.uaContext = uaContext; this.setIconImage(DefaultWindowFactory.getInstance().getDefaultImageIcon(uaContext).getImage()); this.topFormPanel.setMinLabelWidth(100); this.bottomFormPanel.setMinLabelWidth(100); this.bottomFormPanel.setEnabled(false); this.documentField.setCaption("Document:"); this.timeLeftField.setCaption("Estimated time:"); this.mimeTypeField.setCaption("MIME type:"); this.sizeField.setCaption("Size:"); this.destinationField.setCaption("File:"); this.transferSizeField.setCaption("Transfer size:"); this.transferRateField.setCaption("Transfer rate:"); this.openFolderButton.setVisible(false); this.openButton.setVisible(false); this.documentField.setValue(url.toExternalForm()); this.mimeTypeField.setValue(response.getMimeType()); final int cl = response.getContentLength(); this.knownContentLength = cl; final String sizeText = cl == -1 ? "Not known" : getSizeText(cl); this.sizeField.setValue(sizeText); final String estTimeText = (transferSpeed <= 0) || (cl == -1) ? "Not known" : Timing.getElapsedText(cl / transferSpeed); this.timeLeftField.setValue(estTimeText); final Container contentPane = this.getContentPane(); contentPane.setLayout(new FlowLayout()); final Box rootPanel = new Box(BoxLayout.Y_AXIS); rootPanel.setBorder(new EmptyBorder(4, 8, 4, 8)); rootPanel.add(this.progressBar); rootPanel.add(Box.createVerticalStrut(8)); rootPanel.add(this.topFormPanel); rootPanel.add(Box.createVerticalStrut(8)); rootPanel.add(this.bottomFormPanel); rootPanel.add(Box.createVerticalStrut(8)); rootPanel.add(this.getButtonsPanel()); contentPane.add(rootPanel); final FormPanel bfp = this.bottomFormPanel; bfp.addField(this.destinationField); bfp.addField(this.transferRateField); bfp.addField(this.transferSizeField); final FormPanel tfp = this.topFormPanel; tfp.addField(this.documentField); tfp.addField(this.mimeTypeField); tfp.addField(this.sizeField); tfp.addField(this.timeLeftField); final Dimension topPanelPs = this.topFormPanel.getPreferredSize(); this.topFormPanel.setPreferredSize(new Dimension(400, topPanelPs.height)); final Dimension bottomPanelPs = this.bottomFormPanel.getPreferredSize(); this.bottomFormPanel.setPreferredSize(new Dimension(400, bottomPanelPs.height)); this.progressBar.setEnabled(false); this.addWindowListener(new WindowAdapter() { @Override public void windowClosed(final WindowEvent e) { final RequestHandler rh = requestHandler; if (rh != null) { rh.cancel(); // So that there's no error dialog requestHandler = null; } } }); } private Component getButtonsPanel() { final JButton saveButton = this.saveButton; saveButton.setAction(new SaveAction()); saveButton.setText("Save As..."); saveButton.setToolTipText("You must select a file before download begins."); final JButton closeButton = this.closeButton; closeButton.setAction(new CloseAction()); closeButton.setText("Cancel"); final JButton openButton = this.openButton; openButton.setAction(new OpenAction()); openButton.setText("Open"); final JButton openFolderButton = this.openFolderButton; openFolderButton.setAction(new OpenFolderAction()); openFolderButton.setText("Open Folder"); final Box box = new Box(BoxLayout.X_AXIS); // box.setBorder(new BevelBorder(BevelBorder.RAISED)); box.add(Box.createGlue()); box.add(openButton); box.add(Box.createHorizontalStrut(4)); box.add(openFolderButton); box.add(Box.createHorizontalStrut(4)); box.add(saveButton); box.add(Box.createHorizontalStrut(4)); box.add(closeButton); return box; } private void selectFile() { final String path = this.url.getPath(); final int lastSlashIdx = path.lastIndexOf('/'); final String tentativeName = lastSlashIdx == -1 ? path : path.substring(lastSlashIdx + 1); final JFileChooser chooser = new JFileChooser(); final ToolsSettings settings = ToolsSettings.getInstance(); final File directory = settings.getDownloadDirectory(); if (directory != null) { final File selectedFile = new File(directory, tentativeName); chooser.setSelectedFile(selectedFile); } if (chooser.showSaveDialog(this) == JFileChooser.APPROVE_OPTION) { final File file = chooser.getSelectedFile(); if (file.exists()) { if (JOptionPane.showConfirmDialog(this, "The file exists. Are you sure you want to overwrite it?", "Confirm", JOptionPane.YES_NO_OPTION) != JOptionPane.YES_OPTION) { return; } } settings.setDownloadDirectory(file.getParentFile()); settings.save(); this.startDownload(chooser.getSelectedFile()); } } private RequestHandler requestHandler; private File destinationFile; private long downloadBaseTimestamp; private long lastTimestamp; private long lastProgressValue; private double lastTransferRate = Double.NaN; final private UserAgentContext uaContext; private void startDownload(final java.io.File file) { this.saveButton.setEnabled(false); this.timeLeftField.setCaption("Time left:"); this.destinationField.setValue(file.getName()); this.destinationField.setToolTip(file.getAbsolutePath()); this.bottomFormPanel.setEnabled(true); this.bottomFormPanel.revalidate(); final ClientletRequest request = new ClientletRequestImpl(this.url, RequestType.DOWNLOAD); final RequestHandler handler = new DownloadRequestHandler(request, this, file, uaContext); this.destinationFile = file; this.requestHandler = handler; this.downloadBaseTimestamp = System.currentTimeMillis(); final Thread t = new Thread(new DownloadRunnable(handler), "Download:" + this.url.toExternalForm()); t.setDaemon(true); t.start(); } private void doneWithDownload_Safe(final long totalSize) { SwingUtilities.invokeLater(() -> doneWithDownload(totalSize)); } private void doneWithDownload(final long totalSize) { this.requestHandler = null; this.setTitle(this.destinationField.getValue()); this.timeLeftField.setCaption("Download time:"); final long elapsed = System.currentTimeMillis() - this.downloadBaseTimestamp; this.timeLeftField.setValue(Timing.getElapsedText(elapsed)); final String sizeText = getSizeText(totalSize); this.transferSizeField.setValue(sizeText); this.sizeField.setValue(sizeText); if (elapsed > 0) { final double transferRate = (double) totalSize / elapsed; this.transferRateField.setValue(round1(transferRate) + " Kb/sec"); } else { this.transferRateField.setValue("N/A"); } this.progressBar.setIndeterminate(false); this.progressBar.setStringPainted(true); this.progressBar.setValue(100); this.progressBar.setMaximum(100); this.progressBar.setString("Done."); this.closeButton.setText("Close"); if (OS.supportsLaunchPath()) { this.saveButton.setVisible(false); this.openFolderButton.setVisible(true); this.openButton.setVisible(true); this.openButton.revalidate(); } } private void errorInDownload_Safe() { SwingUtilities.invokeLater(() -> errorInDownload()); } private void errorInDownload() { if (this.requestHandler != null) { // If requestHandler is null, it means the download was explicitly // cancelled or the window closed. JOptionPane.showMessageDialog(this, "An error occurred while trying to download the file."); this.dispose(); } } private static double round1(final double value) { return Math.round(value * 10.0) / 10.0; } private static String getSizeText(final long numBytes) { if (numBytes < 1024) { return numBytes + " bytes"; } else { final double numK = numBytes / 1024.0; if (numK < 1024) { return round1(numK) + " Kb"; } else { final double numM = numK / 1024.0; if (numM < 1024) { return round1(numM) + " Mb"; } else { final double numG = numM / 1024.0; return round1(numG) + " Gb"; } } } } private void updateProgress_Safe(final ProgressType progressType, final int value, final int max) { SwingUtilities.invokeLater(() -> updateProgress(progressType, value, max)); } private void updateProgress(final ProgressType progressType, final int value, final int max) { final String sizeText = getSizeText(value); this.transferSizeField.setValue(sizeText); final long newTimestamp = System.currentTimeMillis(); final double lastTransferRate = this.lastTransferRate; final long lastProgressValue = this.lastProgressValue; final long lastTimestamp = this.lastTimestamp; final long elapsed = newTimestamp - lastTimestamp; double newTransferRate = Double.NaN; if (elapsed > 0) { newTransferRate = (value - lastProgressValue) / elapsed; if (!Double.isNaN(lastTransferRate)) { // Weighed average newTransferRate = (newTransferRate + (lastTransferRate * 5.0)) / 6.0; } } if (!Double.isNaN(newTransferRate)) { this.transferRateField.setValue(round1(newTransferRate) + " Kb/sec"); final int cl = this.knownContentLength; if ((cl > 0) && (newTransferRate > 0)) { this.timeLeftField.setValue(Timing.getElapsedText((long) ((cl - value) / newTransferRate))); } } this.lastTimestamp = newTimestamp; this.lastProgressValue = value; this.lastTransferRate = newTransferRate; final JProgressBar pb = this.progressBar; if (progressType == ProgressType.CONNECTING) { pb.setIndeterminate(true); pb.setStringPainted(true); pb.setString("Connecting..."); this.setTitle(this.destinationField.getValue() + ": Connecting..."); } else if (max <= 0) { pb.setIndeterminate(true); pb.setStringPainted(false); this.setTitle(sizeText + " " + this.destinationField.getValue()); } else { final int percent = (value * 100) / max; pb.setIndeterminate(false); pb.setStringPainted(true); pb.setMaximum(max); pb.setValue(value); final String percentText = percent + "%"; pb.setString(percentText); this.setTitle(percentText + " " + this.destinationField.getValue()); } } private class SaveAction extends AbstractAction { private static final long serialVersionUID = -4635141657953704709L; public void actionPerformed(final ActionEvent e) { final String msg = "Downloads are disabled for security reasons.\nWe are working on a novel way to sandbox the browser and will enable downloads after the design is completed."; JOptionPane.showMessageDialog(DownloadDialog.this, msg); // TODO // selectFile(); } } private class OpenFolderAction extends AbstractAction { private static final long serialVersionUID = -6860795246298542670L; public void actionPerformed(final ActionEvent e) { final File file = destinationFile; if (file != null) { try { OS.launchPath(file.getParentFile().getAbsolutePath()); } catch (final Exception thrown) { logger.log(Level.WARNING, "Unable to open folder of file: " + file + ".", thrown); JOptionPane.showMessageDialog(DownloadDialog.this, "An error occurred trying to open the folder."); } } } } private class OpenAction extends AbstractAction { private static final long serialVersionUID = 8435296814556900437L; public void actionPerformed(final ActionEvent e) { final File file = destinationFile; if (file != null) { try { OS.launchPath(file.getAbsolutePath()); DownloadDialog.this.dispose(); } catch (final Exception thrown) { logger.log(Level.WARNING, "Unable to open file: " + file + ".", thrown); JOptionPane.showMessageDialog(DownloadDialog.this, "An error occurred trying to open the file."); } } } } private class CloseAction extends AbstractAction { private static final long serialVersionUID = 5036020829977878826L; public void actionPerformed(final ActionEvent e) { // windowClosedEvent takes care of cancelling download. DownloadDialog.this.dispose(); } } private class DownloadRunnable implements Runnable { private final RequestHandler handler; public DownloadRunnable(final RequestHandler handler) { this.handler = handler; } public void run() { try { RequestEngine.getInstance().inlineRequest(this.handler); } catch (final Exception err) { logger.log(Level.SEVERE, "Unexpected error on download of [" + url.toExternalForm() + "].", err); } } } private class DownloadRequestHandler extends AbstractRequestHandler { private final File file; private boolean downloadDone = false; private long lastProgressUpdate = 0; public DownloadRequestHandler(final ClientletRequest request, final Component dialogComponent, final File file, final UserAgentContext uaContext) { super(request, dialogComponent, uaContext); this.file = file; } @Override public boolean handleException(final ClientletResponse response, final Throwable exception, final RequestType requestType) throws ClientletException { logger.log(Level.WARNING, "An error occurred trying to download " + response.getResponseURL() + " to " + this.file + ".", exception); errorInDownload_Safe(); return true; } @Override public void handleProgress(final ProgressType progressType, final @NonNull URL url, final String method, final int value, final int max) { if (!this.downloadDone) { final long timestamp = System.currentTimeMillis(); if ((timestamp - this.lastProgressUpdate) > 1000) { updateProgress_Safe(progressType, value, max); this.lastProgressUpdate = timestamp; } } } @Override public void processResponse(final ClientletResponse response) throws ClientletException, IOException { try ( final OutputStream out = new FileOutputStream(this.file); final InputStream in = response.getInputStream()) { int totalRead = 0; final byte[] buffer = new byte[8192]; int numRead; while ((numRead = in.read(buffer)) != -1) { if (this.isCancelled()) { throw new IOException("cancelled"); } totalRead += numRead; out.write(buffer, 0, numRead); } this.downloadDone = true; doneWithDownload_Safe(totalRead); } } } }