/*
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);
}
}
}
}