/* * Jitsi, the OpenSource Java VoIP and Instant Messaging client. * * Distributable under LGPL license. * See terms of license at gnu.org. */ package net.java.sip.communicator.plugin.update; import java.awt.*; import java.awt.event.*; import java.io.*; import java.net.*; import java.util.*; import java.util.List; // disambiguation import javax.swing.*; import javax.swing.text.*; import net.java.sip.communicator.service.gui.*; import net.java.sip.communicator.service.httputil.*; import net.java.sip.communicator.service.resources.*; import net.java.sip.communicator.service.update.UpdateService; import net.java.sip.communicator.service.version.*; import net.java.sip.communicator.util.*; import net.java.sip.communicator.util.swing.*; /** * Implements checking for software updates, downloading and applying them i.e. * the very logic of the update plug-in. * * @author Damian Minkov * @author Lyubomir Marinov */ public class Update implements UpdateService { /** * The Logger used by the Update class for logging output. */ private static final Logger logger = Logger.getLogger(Update.class); /** * The name of the property which specifies the update link in the * configuration file. */ private static final String PROP_UPDATE_LINK = "net.java.sip.communicator.UPDATE_LINK"; /** * The link pointing to the ChangeLog of the update. */ private static String changesLink; /** * The JDialog, if any, which is associated with the currently * executing "Check for Updates". While the "Check for Updates" * functionality cannot be entered, clicking the "Check for Updates" menu * item will bring it to the front. */ private static JDialog checkForUpdatesDialog; /** * The link pointing at the download of the update. */ private static String downloadLink; /** * The indicator/counter which determines how many methods are currently * executing the "Check for Updates" functionality so that it is known * whether it can be entered. */ private static int inCheckForUpdates = 0; /** * The latest version of the software found at the configured update * location. */ private static String latestVersion; /** * Invokes "Check for Updates". * * @param notifyAboutNewestVersion true if the user is to be * notified if they have the newest version already; otherwise, * false */ public synchronized void checkForUpdates( final boolean notifyAboutNewestVersion) { if (inCheckForUpdates > 0) { if (checkForUpdatesDialog != null) checkForUpdatesDialog.toFront(); return; } Thread checkForUpdatesThread = new Thread() { @Override public void run() { try { if(isLatestVersion()) { if(notifyAboutNewestVersion) { ResourceManagementService resources = Resources.getResources(); UpdateActivator.getUIService() .getPopupDialog() .showMessagePopupDialog( resources.getI18NString( "plugin.updatechecker.DIALOG_NOUPDATE"), resources.getI18NString( "plugin.updatechecker.DIALOG_NOUPDATE_TITLE"), PopupDialog.INFORMATION_MESSAGE); } } else if (OSUtils.IS_WINDOWS) showWindowsNewVersionAvailableDialog(); else showGenericNewVersionAvailableDialog(); } finally { exitCheckForUpdates(null); } } }; checkForUpdatesThread.setDaemon(true); enterCheckForUpdates(null); try { checkForUpdatesThread.start(); checkForUpdatesThread = null; } finally { if (checkForUpdatesThread != null) exitCheckForUpdates(null); } } /** * Tries to create a new FileOutputStream for a temporary file into * which the setup is to be downloaded. Because temporary files generally * have random characters in their names and the name of the setup may be * shown to the user, first tries to use the name of the URL to be * downloaded because it likely is prettier. * * @param url the URL of the file to be downloaded * @param extension the extension of the File to be created or * null for the default (which may be derived from url) * @param dryRun true to generate a File in * tempFile and not open it or false to generate a * File in tempFile and open it * @param tempFile a File array of at least one element which is to * receive the created File instance at index zero (if successful) * @return the newly created FileOutputStream * @throws IOException if anything goes wrong while creating the new * FileOutputStream */ private static FileOutputStream createTempFileOutputStream( URL url, String extension, boolean dryRun, File[] tempFile) throws IOException { /* * Try to use the name from the URL because it isn't a "randomly" * generated one. */ String path = url.getPath(); File tf = null; FileOutputStream tfos = null; if ((path != null) && (path.length() != 0)) { int nameBeginIndex =path.lastIndexOf('/'); String name; if (nameBeginIndex > 0) { name = path.substring(nameBeginIndex + 1); nameBeginIndex = name.lastIndexOf('\\'); if (nameBeginIndex > 0) name = name.substring(nameBeginIndex + 1); } else name = path; /* * Make sure the extension of the name is EXE so that we're able to * execute it later on. */ int nameLength = name.length(); if (nameLength != 0) { int baseNameEnd = name.lastIndexOf('.'); if (extension == null) extension = ".exe"; if (baseNameEnd == -1) name += extension; else if (baseNameEnd == 0) { if (!extension.equalsIgnoreCase(name)) name += extension; } else name = name.substring(0, baseNameEnd) + extension; try { String tempDir = System.getProperty("java.io.tmpdir"); if ((tempDir != null) && (tempDir.length() != 0)) { tf = new File(tempDir, name); if (!dryRun) tfos = new FileOutputStream(tf); } } catch (FileNotFoundException fnfe) { // Ignore it because we'll try File#createTempFile(). } catch (SecurityException se) { // Ignore it because we'll try File#createTempFile(). } } } // Well, we couldn't use a pretty name so try File#createTempFile(). if ((tfos == null) && !dryRun) { tf = File.createTempFile("setup", ".exe"); tfos = new FileOutputStream(tf); } tempFile[0] = tf; return tfos; } /** * Downloads a remote file specified by its URL into a local file. * * @param url the URL of the remote file to download * @return the local File into which url has been * downloaded or null if there was no response from the * url * @throws IOException if an I/O error occurs during the download */ private static File download(String url) throws IOException { final File[] tempFile = new File[1]; FileOutputStream tempFileOutputStream = null; boolean deleteTempFile = true; tempFileOutputStream = createTempFileOutputStream( new URL(url), /* * The default extension, possibly derived from url, is * fine. Besides, we do not really have information about * any preference. */ null, /* Do create a FileOutputStream. */ false, tempFile); try { HttpUtils.HTTPResponseResult res = HttpUtils.openURLConnection(url); if (res != null) { InputStream content = res.getContent(); // Track the progress of the download. ProgressMonitorInputStream input = new ProgressMonitorInputStream(null, url, content); /* * Set the maximum value of the ProgressMonitor to the size of * the file to download. */ input.getProgressMonitor().setMaximum( (int) res.getContentLength()); try { final BufferedOutputStream output = new BufferedOutputStream(tempFileOutputStream); try { int read = -1; byte[] buff = new byte[1024]; while((read = input.read(buff)) != -1) output.write(buff, 0, read); } finally { output.close(); tempFileOutputStream = null; } deleteTempFile = false; } finally { try { input.close(); } catch (IOException ioe) { /* * Ignore it because we've already downloaded the setup * and that's what matters most. */ } } } } finally { try { if (tempFileOutputStream != null) tempFileOutputStream.close(); } finally { if (deleteTempFile && (tempFile[0] != null)) { tempFile[0].delete(); tempFile[0] = null; } } } return tempFile[0]; } /** * Notifies this UpdateCheckActivator that a method is entering the * "Check for Updates" functionality and it is thus not allowed to enter it * again. * * @param checkForUpdatesDialog the JDialog associated with the * entry in the "Check for Updates" functionality if any. While "Check for * Updates" cannot be entered again, clicking the "Check for Updates" menu * item will bring the checkForUpdatesDialog to the front. */ private static synchronized void enterCheckForUpdates( JDialog checkForUpdatesDialog) { inCheckForUpdates++; if (checkForUpdatesDialog != null) Update.checkForUpdatesDialog = checkForUpdatesDialog; } /** * Notifies this UpdateCheckActivator that a method is exiting the * "Check for Updates" functionality and it may thus be allowed to enter it * again. * * @param checkForUpdatesDialog the JDialog which was associated * with the matching call to {@link #enterCheckForUpdates(JDialog)} if any */ private static synchronized void exitCheckForUpdates( JDialog checkForUpdatesDialog) { if (inCheckForUpdates == 0) throw new IllegalStateException("inCheckForUpdates"); else { inCheckForUpdates--; if ((checkForUpdatesDialog != null) && (Update.checkForUpdatesDialog == checkForUpdatesDialog)) Update.checkForUpdatesDialog = null; } } /** * Gets the current (software) version. * * @return the current (software) version */ private static Version getCurrentVersion() { VersionService verService = ServiceUtils.getService( UpdateActivator.bundleContext, VersionService.class); return verService.getCurrentVersion(); } /** * Determines whether we are currently running the latest version. * * @return true if we are currently running the latest version; * otherwise, false */ private static boolean isLatestVersion() { try { String updateLink = UpdateActivator.getConfiguration().getString( PROP_UPDATE_LINK); if(updateLink == null) { updateLink = Resources.getUpdateConfigurationString("update_link"); } if(updateLink == null) { if (logger.isDebugEnabled()) logger.debug( "Updates are disabled, faking latest version."); } else { HttpUtils.HTTPResponseResult res = HttpUtils.openURLConnection(updateLink); if (res != null) { InputStream in = null; Properties props = new Properties(); try { in = res.getContent(); props.load(in); } finally { in.close(); } latestVersion = props.getProperty("last_version"); downloadLink = props.getProperty("download_link"); changesLink = updateLink.substring( 0, updateLink.lastIndexOf("/") + 1) + props.getProperty("changes_html"); return latestVersion.compareTo( getCurrentVersion().toString()) <= 0; } } } catch (Exception e) { logger.warn( "Could not retrieve latest version or compare it to current" + " version", e); /* * If we get an exception, then we will return that the current * version is the newest one in order to prevent opening the dialog * notifying about the availability of a new version. */ } return true; } /** * Shows dialog informing about the availability of a new version with a * Download button which launches the system Web browser. */ private static void showGenericNewVersionAvailableDialog() { /* * Before showing the dialog, we'll enterCheckForUpdates() in order to * notify that it is not safe to enter "Check for Updates" again. If we * don't manage to show the dialog, we'll have to exitCheckForUpdates(). * If we manage though, we'll have to exitCheckForUpdates() but only * once depending on its modality. */ final boolean[] exitCheckForUpdates = new boolean[] { false }; final JDialog dialog = new SIPCommDialog() { private static final long serialVersionUID = 0L; protected void close(boolean escaped) { synchronized (exitCheckForUpdates) { if (exitCheckForUpdates[0]) exitCheckForUpdates(this); } } }; ResourceManagementService resources = Resources.getResources(); dialog.setTitle( resources.getI18NString("plugin.updatechecker.DIALOG_TITLE")); JEditorPane contentMessage = new JEditorPane(); contentMessage.setContentType("text/html"); contentMessage.setOpaque(false); contentMessage.setEditable(false); String dialogMsg = resources.getI18NString( "plugin.updatechecker.DIALOG_MESSAGE", new String[] { resources.getSettingsString( "service.gui.APPLICATION_NAME") }); if(latestVersion != null) dialogMsg += resources.getI18NString( "plugin.updatechecker.DIALOG_MESSAGE_2", new String[] { resources.getSettingsString( "service.gui.APPLICATION_NAME"), latestVersion }); contentMessage.setText(dialogMsg); JPanel contentPane = new TransparentPanel(new BorderLayout(5,5)); contentPane.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); contentPane.add(contentMessage, BorderLayout.CENTER); JPanel buttonPanel = new TransparentPanel(new FlowLayout(FlowLayout.CENTER, 10, 10)); final JButton closeButton = new JButton( resources.getI18NString( "plugin.updatechecker.BUTTON_CLOSE")); closeButton.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { dialog.dispose(); if (exitCheckForUpdates[0]) exitCheckForUpdates(dialog); } }); if(downloadLink != null) { JButton downloadButton = new JButton( resources.getI18NString( "plugin.updatechecker.BUTTON_DOWNLOAD")); downloadButton.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { if(OSUtils.IS_LINUX64) downloadLink = downloadLink.replace("i386", "amd64"); UpdateActivator.getBrowserLauncher().openURL(downloadLink); /* * Do the same as the Close button in order to not duplicate * the code. */ closeButton.doClick(); } }); buttonPanel.add(downloadButton); } buttonPanel.add(closeButton); contentPane.add(buttonPanel, BorderLayout.SOUTH); dialog.setContentPane(contentPane); dialog.pack(); Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize(); dialog.setLocation( screenSize.width/2 - dialog.getWidth()/2, screenSize.height/2 - dialog.getHeight()/2); synchronized (exitCheckForUpdates) { enterCheckForUpdates(dialog); exitCheckForUpdates[0] = true; } try { dialog.setVisible(true); } finally { synchronized (exitCheckForUpdates) { if (exitCheckForUpdates[0] && dialog.isModal()) exitCheckForUpdates(dialog); } } } /** * Shows dialog informing about new version with button Install * which triggers the update process. */ private static void showWindowsNewVersionAvailableDialog() { /* * Before showing the dialog, we'll enterCheckForUpdates() in order to * notify that it is not safe to enter "Check for Updates" again. If we * don't manage to show the dialog, we'll have to exitCheckForUpdates(). * If we manage though, we'll have to exitCheckForUpdates() but only * once depending on its modality. */ final boolean[] exitCheckForUpdates = new boolean[] { false }; final JDialog dialog = new SIPCommDialog() { private static final long serialVersionUID = 0L; protected void close(boolean escaped) { synchronized (exitCheckForUpdates) { if (exitCheckForUpdates[0]) exitCheckForUpdates(this); } } }; ResourceManagementService resources = Resources.getResources(); dialog.setTitle( resources.getI18NString("plugin.updatechecker.DIALOG_TITLE")); JEditorPane contentMessage = new JEditorPane(); contentMessage.setContentType("text/html"); contentMessage.setOpaque(false); contentMessage.setEditable(false); /* * Use the font of the dialog because contentMessage is just like a * label. */ contentMessage.putClientProperty( JEditorPane.HONOR_DISPLAY_PROPERTIES, Boolean.TRUE); String dialogMsg = resources.getI18NString( "plugin.updatechecker.DIALOG_MESSAGE", new String[] { resources.getSettingsString( "service.gui.APPLICATION_NAME") }); if(latestVersion != null) { dialogMsg += resources.getI18NString( "plugin.updatechecker.DIALOG_MESSAGE_2", new String[] { resources.getSettingsString( "service.gui.APPLICATION_NAME"), latestVersion }); } contentMessage.setText(dialogMsg); JPanel contentPane = new SIPCommFrame.MainContentPane(); contentMessage.setBorder(BorderFactory.createEmptyBorder(10, 0, 20, 0)); contentPane.setBorder(BorderFactory.createEmptyBorder(0, 10, 0, 10)); contentPane.add(contentMessage, BorderLayout.NORTH); JScrollPane scrollChanges = new JScrollPane(); scrollChanges.setPreferredSize(new Dimension(550, 200)); JEditorPane changesHtml = new JEditorPane(); changesHtml.setContentType("text/html"); changesHtml.setEditable(false); changesHtml.setBorder(BorderFactory.createLoweredBevelBorder()); scrollChanges.setViewportView(changesHtml); contentPane.add(scrollChanges, BorderLayout.CENTER); try { Document changesHtmlDocument = changesHtml.getDocument(); if (changesHtmlDocument instanceof AbstractDocument) { ((AbstractDocument) changesHtmlDocument) .setAsynchronousLoadPriority(0); } changesHtml.setPage(new URL(changesLink)); } catch (Exception e) { logger.error("Cannot set changes Page", e); } JPanel buttonPanel = new TransparentPanel(new FlowLayout(FlowLayout.CENTER, 10, 10)); final JButton closeButton = new JButton( resources.getI18NString( "plugin.updatechecker.BUTTON_CLOSE")); closeButton.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { dialog.dispose(); if (exitCheckForUpdates[0]) exitCheckForUpdates(dialog); } }); if(downloadLink != null) { JButton installButton = new JButton( resources.getI18NString( "plugin.updatechecker.BUTTON_INSTALL")); installButton.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { if(OSUtils.IS_WINDOWS64) downloadLink = downloadLink.replace("x86", "x64"); enterCheckForUpdates(null); try { /* * Do the same as the Close button in order to not * duplicate the code. */ closeButton.doClick(); } finally { boolean windowsUpdateThreadHasStarted = false; try { new Thread() { @Override public void run() { try { windowsUpdate(); } finally { exitCheckForUpdates(null); } } }.start(); windowsUpdateThreadHasStarted = true; } finally { if (!windowsUpdateThreadHasStarted) exitCheckForUpdates(null); } } } }); buttonPanel.add(installButton); } buttonPanel.add(closeButton); contentPane.add(buttonPanel, BorderLayout.SOUTH); dialog.setContentPane(contentPane); dialog.pack(); Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize(); dialog.setLocation( screenSize.width/2 - dialog.getWidth()/2, screenSize.height/2 - dialog.getHeight()/2); synchronized (exitCheckForUpdates) { enterCheckForUpdates(dialog); exitCheckForUpdates[0] = true; } try { dialog.setVisible(true); } finally { synchronized (exitCheckForUpdates) { if (exitCheckForUpdates[0] && dialog.isModal()) exitCheckForUpdates(dialog); } } } /** * Implements the very update procedure on Windows which includes without * being limited to: *