package com.mobilesorcery.sdk.update.internal;
import java.io.DataInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.StringWriter;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.nio.charset.Charset;
import java.text.MessageFormat;
import java.util.HashMap;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.jface.dialogs.IDialogConstants;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.jface.dialogs.MessageDialogWithToggle;
import org.eclipse.jface.util.Policy;
import org.eclipse.swt.SWT;
import org.eclipse.swt.browser.Browser;
import org.eclipse.swt.browser.LocationEvent;
import org.eclipse.swt.browser.LocationListener;
import org.eclipse.swt.dnd.Clipboard;
import org.eclipse.swt.dnd.TextTransfer;
import org.eclipse.swt.dnd.Transfer;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.ui.IPerspectiveDescriptor;
import org.eclipse.ui.IViewPart;
import org.eclipse.ui.IWindowListener;
import org.eclipse.ui.IWorkbench;
import org.eclipse.ui.IWorkbenchPage;
import org.eclipse.ui.IWorkbenchWindow;
import org.eclipse.ui.PartInitException;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.WorkbenchException;
import org.eclipse.ui.intro.IIntroManager;
import org.eclipse.ui.intro.IIntroPart;
import com.mobilesorcery.sdk.core.CoreMoSyncPlugin;
import com.mobilesorcery.sdk.core.IUpdater;
import com.mobilesorcery.sdk.core.MoSyncTool;
import com.mobilesorcery.sdk.core.Util;
import com.mobilesorcery.sdk.core.stats.Variables;
import com.mobilesorcery.sdk.ui.MosyncUIPlugin;
import com.mobilesorcery.sdk.ui.UIUtils;
import com.mobilesorcery.sdk.update.MosyncUpdatePlugin;
import com.mobilesorcery.sdk.update.UpdateManager;
import com.mobilesorcery.sdk.update.UpdateManagerBase;
/**
* An updater for the 'new' update process; 'new' is obviously new only for a
* certain period of time, so if you're reading this javadoc it's probably older
* than the flashy new-and-fresh-car feeling conveyed by the 'new' word.
*
* @author Mattias Bybro, mattias.bybro@mosync.com
*
*/
public class DefaultUpdater2 extends UpdateManagerBase implements IUpdater {
public class InternalLocationListener implements LocationListener {
private final IViewPart view;
public InternalLocationListener(IViewPart view) {
this.view = view;
}
@Override
public void changed(LocationEvent event) {
}
@Override
public void changing(LocationEvent event) {
String location = event.location;
// Specially formatted urls will kill the editor.
try {
URL locationURL = new URL(location);
boolean isKillURL = locationURL.getPath().contains(
"close-ide-registration");
if (isKillURL) {
view.getSite().getWorkbenchWindow().getActivePage()
.hideView(view);
}
} catch (MalformedURLException e) {
// Just ignore.
}
}
}
public static final String SHOW_CONNECTION_FAILED_POPUP = "show.conn.fail.popup";
public class OpenBrowserRunnable implements Runnable {
private final URL whereToGo;
private final String name;
private boolean reopenIntro;
public OpenBrowserRunnable(URL whereToGo, String name) {
this.whereToGo = whereToGo;
this.name = name;
}
@Override
public void run() {
try {
// Since we implicitly open a perspective we do not want
// the "re-open welcome screen" flag cleared; hence this
// listener deactivation.
perspectiveListener.setActive(false);
IWorkbenchWindow window = PlatformUI.getWorkbench()
.getActiveWorkbenchWindow();
IIntroPart currentIntroPart = PlatformUI.getWorkbench()
.getIntroManager().getIntro();
reopenIntro = currentIntroPart != null;
if (reopenIntro) {
PlatformUI.getWorkbench().getIntroManager()
.closeIntro(currentIntroPart);
}
try {
IWorkbench wb = PlatformUI.getWorkbench();
IWorkbenchPage page = wb.showPerspective(RegistrationPerspectiveFactory.REGISTRATION_PERSPECTIVE_ID,
window);
} catch (WorkbenchException e) {
// We can still do SOMETHING.
CoreMoSyncPlugin.getDefault().log(e);
}
IViewPart view = RegistrationWebBrowserView.open(whereToGo,
reopenIntro);
Browser browser = RegistrationWebBrowserView.getBrowser(view);
if (browser != null) {
InternalLocationListener locationListener = new InternalLocationListener(
view);
browser.addLocationListener(locationListener);
}
} catch (PartInitException e) {
e.printStackTrace();
openFallbackDialog(whereToGo, name);
} finally {
perspectiveListener.setActive(true);
}
}
}
class UpdateJob extends Job {
public UpdateJob() {
super("Updating Profile Database");
}
@Override
protected IStatus run(IProgressMonitor monitor) {
try {
UpdateManager.getDefault().downloadProfileUpdate(monitor);
if (showConfirmDialog()) {
UpdateManager.getDefault().runUpdater(
new NullProgressMonitor());
}
} catch (Exception e) {
return new Status(IStatus.ERROR, MosyncUpdatePlugin.PLUGIN_ID,
"Could not download updated profile database", e);
}
return null;
}
private boolean showConfirmDialog() {
final Display d = PlatformUI.getWorkbench().getDisplay();
final boolean[] result = new boolean[1];
d.syncExec(new Runnable() {
@Override
public void run() {
Shell shell = new Shell(d);
result[0] = MessageDialog
.openConfirm(
shell,
"Restart?",
"A new profile database has been downloaded. To activate the new profiles, the IDE must be restarted");
shell.dispose();
}
});
return result[0];
}
}
class UpdateRunnable implements Runnable {
private final boolean isStartedByUser;
private final boolean registrationOnly;
public UpdateRunnable(boolean isStartedByUser, boolean registrationOnly) {
this.isStartedByUser = isStartedByUser;
this.registrationOnly = registrationOnly;
}
@Override
public void run() {
MosyncUIPlugin.getDefault().awaitWorkbenchStartup(null);
String userKey = MoSyncTool.getDefault().getProperty(
MoSyncTool.USER_HASH_PROP_2);
try {
boolean requestNewKey = Util.isEmpty(userKey);
if (!Util.isEmpty(userKey)) {
boolean isValidKey = validateKey();
if (!isValidKey) {
MoSyncTool.getDefault().setProperty(
MoSyncTool.USER_HASH_PROP_2, null);
showInvalidKeyWarning();
}
requestNewKey |= !isValidKey;
}
if (requestNewKey) {
userKey = requestKeyFromServer();
MoSyncTool.getDefault().setProperty(
MoSyncTool.USER_HASH_PROP_2, userKey);
}
int userStatus = getUserStatus();
switch (userStatus) {
case USER_NOT_CONFIRMED:
userNotConfirmedAction();
break;
case USER_NOT_REGISTERED:
userNotRegisteredAction();
break;
case USER_ACTIVATED:
if (registrationOnly) {
userAlreadyRegisteredAction();
} else if (isStartedByUser || shouldPerformAutoUpdate()) {
performUpdateAction(isStartedByUser);
}
}
} catch (IOException e) {
if (isStartedByUser || shouldPopupConnectionFailedMessage()) {
popupConnectionFailedDialog(isStartedByUser);
}
}
}
}
private final RegistrationPartListener perspectiveListener = new RegistrationPartListener(
null, false);
private WindowListener windowListener;
private final class WindowListener implements IWindowListener {
@Override
public void windowOpened(IWorkbenchWindow window) {
}
@Override
public void windowDeactivated(IWorkbenchWindow window) {
detachPerspectiveListener(window);
}
@Override
public void windowClosed(IWorkbenchWindow window) {
// TODO Auto-generated method stub
}
@Override
public void windowActivated(IWorkbenchWindow window) {
attachPerspectiveListener(window);
}
}
public boolean shouldPopupConnectionFailedMessage() {
return MessageDialogWithToggle.ALWAYS.equals(MosyncUpdatePlugin
.getDefault().getPreferenceStore()
.getString(SHOW_CONNECTION_FAILED_POPUP));
}
public void showInvalidKeyWarning() {
final Display d = PlatformUI.getWorkbench().getDisplay();
d.asyncExec(new Runnable() {
@Override
public void run() {
Shell shell = PlatformUI.getWorkbench()
.getActiveWorkbenchWindow().getShell();
MessageDialog.openWarning(shell, "Invalid user key",
"Your registration key is invalid.\n"
+ "Please re-register.");
}
});
}
public void shouldPopupConnectionFailedMessage(
boolean shouldPopupConnectionFailedMessage) {
// Why doesn't the dialog do this properly - investigate later, for now
// just make it work.
MosyncUpdatePlugin
.getDefault()
.getPreferenceStore()
.setValue(
SHOW_CONNECTION_FAILED_POPUP,
shouldPopupConnectionFailedMessage ? MessageDialogWithToggle.ALWAYS
: MessageDialogWithToggle.NEVER);
}
public DefaultUpdater2() {
initPerspectiveListener();
}
@Override
public void dispose() {
PlatformUI.getWorkbench().removeWindowListener(windowListener);
}
public void initPerspectiveListener() {
windowListener = new WindowListener();
PlatformUI.getWorkbench().addWindowListener(windowListener);
}
protected void attachPerspectiveListener(IWorkbenchWindow window) {
window.addPerspectiveListener(perspectiveListener);
}
protected void detachPerspectiveListener(IWorkbenchWindow window) {
window.removePerspectiveListener(perspectiveListener);
}
public void closeRegistrationPerspective() {
perspectiveListener.closeRegistrationPerspective();
}
@Override
public void update(boolean isStartedByUser) {
startUpdateRunnable(isStartedByUser, false);
}
@Override
public void register(boolean isStartedByUser) {
startUpdateRunnable(isStartedByUser, true);
}
private void startUpdateRunnable(boolean isStartedByUser,
boolean registerOnly) {
if (isStartedByUser
|| !MosyncUIPlugin.getDefault().isExampleWorkspace()) {
UpdateRunnable updater = new UpdateRunnable(isStartedByUser,
registerOnly);
Thread updateThread = new Thread(updater,
"Registration and/or update");
updateThread.start();
}
}
private void performUpdateAction(boolean isStartedByUser)
throws IOException {
try {
if (UpdateManager.getDefault().isUpdateAvailable()) {
// MOSYNC-1548: Do not perform automatic updates;
// show a dialog instead.
/*
* Old code, for reference: UpdateJob updateJob = new
* UpdateJob(); updateJob.setUser(isStartedByUser);
* updateJob.schedule();
*/
showUpdatesDialog(true);
} else if (isStartedByUser) {
showUpdatesDialog(false);
}
} catch (Exception e) {
if (e instanceof IOException) {
throw (IOException) e;
} else {
throw new IOException("Could not connect", e);
}
}
}
private void showUpdatesDialog(final boolean thereAreUpdates) {
final Display d = PlatformUI.getWorkbench().getDisplay();
d.asyncExec(new Runnable() {
@Override
public void run() {
innerShowUpdatesDialog(d, thereAreUpdates);
}
});
}
private void innerShowUpdatesDialog(Display d, boolean thereAreUpdates) {
try {
Shell shell = new Shell(d);
if (thereAreUpdates) {
MessageDialog dialog = new MessageDialog(shell,
"There are updates", null,
"There is a new version of MoSync available",
MessageDialog.INFORMATION, new String[] {
"Go To Download Site",
IDialogConstants.OK_LABEL }, 1);
if (dialog.open() == 0) {
PlatformUI.getWorkbench().getBrowserSupport()
.getExternalBrowser()
.openURL(getRequestURL("download", null));
}
} else {
MessageDialog.openInformation(shell, "No updates",
"No updates found");
}
shell.dispose();
} catch (Exception e) {
Policy.getStatusHandler()
.show(new Status(IStatus.ERROR, "Internal error",
e.getMessage()), "");
}
}
public void popupConnectionFailedDialog(final boolean isStartedByUser) {
final Display d = PlatformUI.getWorkbench().getDisplay();
d.asyncExec(new Runnable() {
@Override
public void run() {
popupConnectionFailedDialogSync(d, isStartedByUser);
}
});
}
private void popupConnectionFailedDialogSync(Display d,
boolean isStartedByUser) {
Shell shell = PlatformUI.getWorkbench().getActiveWorkbenchWindow()
.getShell();
// Shell shell = new Shell(d);
String dialogTitle = "Could not connect";
String msg = "MoSync was unable to connect to its update server.\n"
+ "If you would like the latest device profiles and updates you will need to be connected to the Internet.\n"
+ "Check your Internet connection and your firewall settings.";
try {
if (isStartedByUser) {
MessageDialog.openInformation(shell, dialogTitle, msg);
} else {
MessageDialogWithToggle dialog = MessageDialogWithToggle.open(
MessageDialogWithToggle.INFORMATION, shell,
dialogTitle, msg,
"Show this message if no connection is found",
shouldPopupConnectionFailedMessage(),
MosyncUpdatePlugin.getDefault().getPreferenceStore(),
SHOW_CONNECTION_FAILED_POPUP, SWT.SHELL_TRIM);
if (dialog.getReturnCode() != -1) {
shouldPopupConnectionFailedMessage(dialog.getToggleState());
}
}
} finally {
// shell.dispose();
}
}
private void userNotRegisteredAction() throws IOException {
launchInternalBrowser(getInitialRegistrationRequestURL(), "Register");
}
private URL getInitialRegistrationRequestURL() throws MalformedURLException {
return getRequestURL("registration/register/", MosyncUIPlugin
.getDefault().getVersionParameters(true));
}
private void userNotConfirmedAction() throws IOException {
launchInternalBrowser(
getRequestURL("registration/confirmation/", MosyncUIPlugin
.getDefault().getVersionParameters(true)),
"Confirm Registration");
}
private void userAlreadyRegisteredAction() throws IOException {
// The web server handles this in the same way if the user is NOT
// registered.
userNotRegisteredAction();
}
private void launchInternalBrowser(URL whereToGo, String name) {
PlatformUI.getWorkbench().getDisplay()
.syncExec(new OpenBrowserRunnable(whereToGo, name));
}
private void openFallbackDialog(URL whereToGo, String name) {
Shell shell = PlatformUI.getWorkbench().getActiveWorkbenchWindow()
.getShell();
String message = MessageFormat.format(
"Unable to launch browser. To {0}, enter this URL"
+ "into a web browser: {1}", name, whereToGo);
MessageDialog dialog = new MessageDialog(shell,
"Unable to launch browser", null, message, MessageDialog.ERROR,
new String[] { "Copy web URL to Clipboard", "Ok" }, 1);
int result = dialog.open();
if (result == 0 /* copy to clipboard */) {
Clipboard clipboard = new Clipboard(shell.getDisplay());
try {
clipboard.setContents(
new Object[] { whereToGo.toExternalForm() },
new Transfer[] { TextTransfer.getInstance() });
} finally {
clipboard.dispose();
}
}
}
private final static int readUntilEOF(byte[] buffer, InputStream input) throws IOException {
int len = 0;
int res;
while(len < buffer.length) {
res = input.read(buffer, len, buffer.length - len);
if(res <= 0)
break;
len += res;
}
return len;
}
public final static int USER_NOT_CONFIRMED = 0;
public final static int USER_ACTIVATED = 1;
public final static int USER_NOT_REGISTERED = 2;
private static final int MAX_KEY_LENGTH = 8192;
public String requestKeyFromServer() throws IOException {
Response response = sendRequest(getRequestURL(
"registration/request/key", null));
byte[] buffer = new byte[MAX_KEY_LENGTH];
try {
InputStream input = response.getContent();
int len = readUntilEOF(buffer, input);
return new String(buffer, 0, len);
} finally {
response.close();
}
}
public boolean validateKey() throws IOException {
Response validated = sendRequest(getRequestURL(
"registration/request/validate", MosyncUIPlugin.getDefault()
.getVersionParameters(false)));
boolean isValid = getBooleanResponse(validated,
"Server failed to accept user key validation request");
return isValid;
}
public int getUserStatus() throws IOException {
Response response = sendRequest(getRequestURL(
"registration/request/userstatus", MosyncUIPlugin.getDefault()
.getVersionParameters(false)));
InputStream input = null;
try {
input = response.getContent();
int result = input.read();
if (Character.isDigit(result)) {
return Integer.parseInt(Character.toString((char) result));
}
throw new UpdateException(MessageFormat.format(
"Invalid response: {0} (character: {1})",
Integer.toHexString(result), (char) result));
} finally {
response.close();
}
}
public static String getInitialURL() {
IUpdater updater = CoreMoSyncPlugin.getDefault().getUpdater();
if (updater instanceof DefaultUpdater2) {
try {
return ((DefaultUpdater2) updater)
.getInitialRegistrationRequestURL().toExternalForm();
} catch (MalformedURLException e) {
// That's ok -- we'll log this elsewhere.
}
}
return "";
}
@Override
public void sendStats(String stats) throws IOException {
OutputStream output = null;
InputStream input = null;
HttpURLConnection connection = null;
try {
// Yep, we use .php
URL statsURL = getRequestURL("stats.php", null);
// HTTP POST - fake HTML form :)
connection = (HttpURLConnection) statsURL.openConnection();
connection.setDoInput(true);
connection.setDoOutput(true);
connection.setUseCaches(false);
connection.setRequestProperty("Content-Type",
"application/x-www-form-urlencoded");
HashMap<String, String> params = new HashMap<String, String>();
params.putAll(MosyncUIPlugin.getDefault().getVersionParameters());
params.put("stats", stats);
String postRequest = Util.toGetUrl(null, params);
byte[] postRequestData = postRequest.getBytes();
connection.setRequestProperty("Content-Length", Integer.toString(postRequestData.length));
output = connection.getOutputStream();
output.write(postRequestData);
output.flush();
input = connection.getInputStream();
int code = connection.getResponseCode();
if (CoreMoSyncPlugin.getDefault().isDebugging()) {
Util.transfer(input, System.err);
}
if (code != HttpURLConnection.HTTP_OK) {
throw new IOException("Stats server did not accept stats data.");
}
} finally {
Util.safeClose(output);
Util.safeClose(input);
}
}
}