/*
* Syncany, www.syncany.org
* Copyright (C) 2011-2015 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.operations.gui;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.syncany.Client;
import org.syncany.config.GuiEventBus;
import org.syncany.config.UserConfig;
import org.syncany.gui.util.I18n;
import org.syncany.operations.daemon.messages.PluginManagementRequest;
import org.syncany.operations.daemon.messages.PluginManagementResponse;
import org.syncany.operations.daemon.messages.UpdateManagementRequest;
import org.syncany.operations.daemon.messages.UpdateManagementResponse;
import org.syncany.operations.plugin.ExtendedPluginInfo;
import org.syncany.operations.plugin.PluginInfo;
import org.syncany.operations.plugin.PluginOperationAction;
import org.syncany.operations.plugin.PluginOperationOptions;
import org.syncany.operations.plugin.PluginOperationOptions.PluginListMode;
import org.syncany.operations.plugin.PluginOperationResult;
import org.syncany.operations.update.AppInfo;
import org.syncany.operations.update.UpdateOperationAction;
import org.syncany.operations.update.UpdateOperationOptions;
import org.syncany.operations.update.UpdateOperationResult;
import org.syncany.plugins.gui.GuiPlugin;
import org.syncany.util.StringUtil;
import com.google.common.eventbus.Subscribe;
/**
* The update checker can contact the Syncany server to check for available
* application and plugin updates. It can either trigger a check immediately
* ({@link #check()}) or regularly in an update check ({@link #start()}). Instead
* of contacting the server directly, this class communicates only with the daemon.
*
* <p>Responses from the update check are delivered via the {@link UpdateCheckListener}
* interface.
*
* @author Philipp C. Heckel <philipp.heckel@gmail.com>
*/
public class UpdateChecker {
private static final Logger logger = Logger.getLogger(UpdateChecker.class.getSimpleName());
private static int UPDATE_CHECK_DELAY = 2*60*1000; // Wait a while after start
private static int UPDATE_CHECK_TIMER_INTERVAL = 1*60*60*1000; // Check every 1 hour
private static int UPDATE_CHECK_API_INTERVAL = 1*24*60*60*1000; // Check every 24 hours
private UpdateCheckListener listener;
private GuiEventBus eventBus;
private File userUpdateFile;
private UpdateManagementRequest updateRequest;
private UpdateManagementResponse updateResponse;
private PluginManagementRequest pluginRequest;
private PluginManagementResponse pluginResponse;
public UpdateChecker(UpdateCheckListener listener) {
this.listener = listener;
this.eventBus = GuiEventBus.getAndRegister(this);
this.userUpdateFile = new File(UserConfig.getUserPluginsUserdataDir(GuiPlugin.ID), "update");
this.updateRequest = null;
this.updateResponse = null;
this.pluginRequest = null;
this.pluginResponse = null;
}
/**
* Start update timer to regularly check for updates. A background
* thread will check for updates every 24h.
*/
public void start() {
logger.log(Level.INFO, "Update check: Starting update timer ...");
new Timer("UpdateTimer").schedule(new TimerTask() {
@Override
public void run() {
checkUpdatesIfNecessary();
}
}, UPDATE_CHECK_DELAY, UPDATE_CHECK_TIMER_INTERVAL);
}
/**
* Checks for updates immediately.
*/
public void check() {
logger.log(Level.INFO, "Update check: Checking for updates NOW ...");
checkAppUpdates();
checkPluginUpdates();
touchUpdateFile();
}
private void touchUpdateFile() {
try {
if (!userUpdateFile.exists()) {
userUpdateFile.createNewFile();
}
userUpdateFile.setLastModified(System.currentTimeMillis());
}
catch (Exception e) {
logger.log(Level.WARNING, "Update check: Cannot create update file.", e);
}
}
private void checkAppUpdates() {
logger.log(Level.INFO, "Update Check: Sending update management request ...");
UpdateOperationOptions updateOperationOptions = new UpdateOperationOptions();
updateOperationOptions.setAction(UpdateOperationAction.CHECK);
updateRequest = new UpdateManagementRequest(updateOperationOptions);
eventBus.post(updateRequest);
}
private void checkPluginUpdates() {
logger.log(Level.INFO, "Update Check: Sending plugin list management request ...");
PluginOperationOptions pluginOperationOptions = new PluginOperationOptions();
pluginOperationOptions.setAction(PluginOperationAction.LIST);
pluginOperationOptions.setListMode(PluginListMode.ALL);
pluginRequest = new PluginManagementRequest(pluginOperationOptions);
eventBus.post(pluginRequest);
}
@Subscribe
public void onUpdateResultReceived(UpdateManagementResponse updateResponse) {
boolean isMatchingResponse = updateRequest != null && updateRequest.getId() == updateResponse.getRequestId();
if (isMatchingResponse) {
this.updateResponse = updateResponse;
if (this.updateResponse != null && this.pluginResponse != null) {
fireUpdateResponse();
}
}
}
@Subscribe
public void onPluginResultReceived(PluginManagementResponse pluginResponse) {
boolean isMatchingResponse = pluginRequest != null && pluginRequest.getId() == pluginResponse.getRequestId();
if (isMatchingResponse) {
this.pluginResponse = pluginResponse;
if (this.updateResponse != null && this.pluginResponse != null) {
fireUpdateResponse();
}
}
}
private void fireUpdateResponse() {
String updateResponseText = getUpdateText();
boolean updatesAvailable = isUpdateAvailable();
logger.log(Level.INFO, "Update Check: Updates available (= " + updatesAvailable + "); text: " + updateResponseText);
listener.updateResponseReceived(updateResponse, pluginResponse, updateResponseText, updatesAvailable);
updateResponse = null;
pluginResponse = null;
}
private void checkUpdatesIfNecessary() {
if (!userUpdateFile.exists()) {
logger.log(Level.INFO, "Update check: No update file (first run), so no update check necessary. Next file check in " + (UPDATE_CHECK_TIMER_INTERVAL/1000/60) + "min.");
touchUpdateFile();
}
else if (System.currentTimeMillis() - userUpdateFile.lastModified() > UPDATE_CHECK_API_INTERVAL) {
logger.log(Level.INFO, "Update check: Necessary, because last check is longer than " + (UPDATE_CHECK_API_INTERVAL/1000/60/60) + "h ago. Next file check in " + (UPDATE_CHECK_TIMER_INTERVAL/1000/60) + "min.");
check();
}
else {
logger.log(Level.INFO, "Update check: Not necessary, last check less than " + (UPDATE_CHECK_API_INTERVAL/1000/60/60) + "h ago. Next file check in " + (UPDATE_CHECK_TIMER_INTERVAL/1000/60) + "min.");
}
}
private boolean isUpdateAvailable() {
return isAppUpdateAvailable() || isPluginUpdateAvailable();
}
private boolean isAppUpdateAvailable() {
return updateResponse != null && updateResponse.getResult() != null && updateResponse.getResult().isNewVersionAvailable();
}
private boolean isPluginUpdateAvailable() {
if (pluginResponse == null || pluginResponse.getResult() == null) {
return false;
}
else {
for (ExtendedPluginInfo extPluginInfo : pluginResponse.getResult().getPluginList()) {
if (extPluginInfo.isOutdated()) {
return true;
}
}
return false;
}
}
private String getUpdateText() {
return getAppUpdateText() + " " + getPluginUpdateText();
}
private String getAppUpdateText() {
UpdateOperationResult updateResult = updateResponse.getResult();
AppInfo appInfo = updateResult.getAppInfo();
if (updateResult.isNewVersionAvailable()) {
return I18n.getText("org.syncany.operations.gui.UpdateChecker.updates.app.newVersionAvailable", appInfo.getAppVersion());
}
else {
return I18n.getText("org.syncany.operations.gui.UpdateChecker.updates.app.upToDate", Client.getApplicationVersion());
}
}
private String getPluginUpdateText() {
PluginOperationResult pluginResult = pluginResponse.getResult();
List<String> outdatedPluginNames = new ArrayList<>();
for (ExtendedPluginInfo extPluginInfo : pluginResult.getPluginList()) {
PluginInfo pluginInfo = (extPluginInfo.isInstalled()) ? extPluginInfo.getLocalPluginInfo() : extPluginInfo.getRemotePluginInfo();
if (extPluginInfo.isOutdated()) {
outdatedPluginNames.add(pluginInfo.getPluginName());
}
}
if (outdatedPluginNames.size() == 0) {
return I18n.getText("org.syncany.operations.gui.UpdateChecker.updates.plugins.upToDate");
}
else if (outdatedPluginNames.size() == 1) {
String pluginNameText = outdatedPluginNames.get(0);
return I18n.getText("org.syncany.operations.gui.UpdateChecker.updates.plugins.oneOutdated", pluginNameText);
}
else {
String pluginsNamesText = StringUtil.join(outdatedPluginNames, ", ");
return I18n.getText("org.syncany.operations.gui.UpdateChecker.updates.plugins.manyOutdated", pluginsNamesText);
}
}
public void dispose() {
eventBus.unregister(this);
}
public interface UpdateCheckListener {
public void updateResponseReceived(UpdateManagementResponse appResponse, PluginManagementResponse pluginResponse, String updateResponseText,
boolean updatesAvailable);
}
}