diff --git a/plugin/META-INF/MANIFEST.MF b/plugin/META-INF/MANIFEST.MF index f00b620d..88843d2f 100644 --- a/plugin/META-INF/MANIFEST.MF +++ b/plugin/META-INF/MANIFEST.MF @@ -9,6 +9,7 @@ Automatic-Module-Name: amazon.q.eclipse Bundle-ActivationPolicy: lazy Bundle-Activator: software.aws.toolkits.eclipse.amazonq.plugin.Activator Require-Bundle: org.eclipse.core.runtime;bundle-version="3.31.0", + org.tukaani.xz;bundle-version="1.9.0", org.eclipse.ui;bundle-version="3.205.100", org.eclipse.core.resources;bundle-version="3.20.100", org.eclipse.jface.text;bundle-version="3.25.100", diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/LspStartupActivity.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/LspStartupActivity.java index 07387fc3..5995f715 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/LspStartupActivity.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/LspStartupActivity.java @@ -25,6 +25,7 @@ import software.aws.toolkits.eclipse.amazonq.telemetry.metadata.ExceptionMetadata; import software.aws.toolkits.eclipse.amazonq.views.ViewConstants; import software.aws.toolkits.eclipse.amazonq.util.ToolkitNotification; +import software.aws.toolkits.eclipse.amazonq.util.UpdateUtils; import org.eclipse.mylyn.commons.ui.dialogs.AbstractNotificationPopup; import software.aws.toolkits.eclipse.amazonq.views.actions.ToggleAutoTriggerContributionItem; import org.eclipse.lsp4e.LanguageServiceAccessor; @@ -55,6 +56,20 @@ protected IStatus run(final IProgressMonitor monitor) { Activator.getLspProvider().getAmazonQServer(); this.launchWebview(); } + Job updateCheckJob = new Job("Check for updates") { + @Override + protected IStatus run(final IProgressMonitor monitor) { + try { + UpdateUtils.getInstance().checkForUpdate(); + } catch (Exception e) { + return new Status(IStatus.WARNING, "amazonq", "Failed to check for updates", e); + } + return Status.OK_STATUS; + } + }; + + updateCheckJob.setPriority(Job.DECORATE); + updateCheckJob.schedule(); } private void launchWebview() { diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/util/Constants.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/util/Constants.java index c8cbc518..f0a6b875 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/util/Constants.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/util/Constants.java @@ -15,6 +15,10 @@ private Constants() { public static final String LSP_OPT_OUT_TELEMETRY_CONFIGURATION_KEY = "optOutTelemetry"; public static final String LSP_Q_CONFIGURATION_KEY = "aws.q"; public static final String LSP_CW_CONFIGURATION_KEY = "aws.codeWhisperer"; + public static final String DO_NOT_SHOW_UPDATE_KEY = "doNotShowUpdate"; + public static final String PLUGIN_UPDATE_NOTIFICATION_TITLE = "Amazon Q Update Available"; + public static final String PLUGIN_UPDATE_NOTIFICATION_BODY = "Amazon Q plugin version %s is available." + + "Please update to receive the latest features and bug fixes."; public static final String LSP_CW_OPT_OUT_KEY = "shareCodeWhispererContentWithAWS"; public static final String LSP_CODE_REFERENCES_OPT_OUT_KEY = "includeSuggestionsWithCodeReferences"; public static final String IDE_CUSTOMIZATION_NOTIFICATION_TITLE = "Amazon Q Customization"; diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/util/PersistentToolkitNotification.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/util/PersistentToolkitNotification.java new file mode 100644 index 00000000..c586e71a --- /dev/null +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/util/PersistentToolkitNotification.java @@ -0,0 +1,85 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package software.aws.toolkits.eclipse.amazonq.util; + +import org.eclipse.swt.events.MouseAdapter; +import org.eclipse.swt.events.MouseEvent; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.graphics.Color; +import org.eclipse.swt.graphics.Font; +import org.eclipse.swt.graphics.FontData; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Label; + +import java.util.function.Consumer; +import org.eclipse.swt.SWT; + +public final class PersistentToolkitNotification extends ToolkitNotification { + private final Consumer checkboxCallback; + + public PersistentToolkitNotification(final Display display, final String title, + final String description, final Consumer checkboxCallback) { + super(display, title, description); + this.checkboxCallback = checkboxCallback; + } + + @Override + protected void createContentArea(final Composite parent) { + super.createContentArea(parent); + + Composite container = (Composite) parent.getChildren()[0]; + + // create checkbox + Button doNotShowCheckbox = new Button(container, SWT.CHECK); + GridData checkboxGridData = new GridData(SWT.BEGINNING, SWT.CENTER, false, false); + doNotShowCheckbox.setLayoutData(checkboxGridData); + + // create checkbox button text + Label checkboxLabel = new Label(container, SWT.NONE); + checkboxLabel.setText("Don't show this message again"); + checkboxLabel.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false)); + + // style button text + Color grayColor = new Color(parent.getDisplay(), 128, 128, 128); + checkboxLabel.setForeground(grayColor); + + Font originalFont = checkboxLabel.getFont(); + FontData[] fontData = originalFont.getFontData(); + for (FontData fd : fontData) { + fd.setHeight(fd.getHeight() - 1); + } + Font smallerFont = new Font(parent.getDisplay(), fontData); + checkboxLabel.setFont(smallerFont); + + checkboxLabel.addDisposeListener(e -> { + smallerFont.dispose(); + grayColor.dispose(); + }); + + // make button text clickable + checkboxLabel.addMouseListener(new MouseAdapter() { + @Override + public void mouseUp(final MouseEvent e) { + doNotShowCheckbox.setSelection(!doNotShowCheckbox.getSelection()); + if (checkboxCallback != null) { + checkboxCallback.accept(doNotShowCheckbox.getSelection()); + } + } + }); + + doNotShowCheckbox.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(final SelectionEvent e) { + if (checkboxCallback != null) { + checkboxCallback.accept(doNotShowCheckbox.getSelection()); + } + } + }); + } + +} diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/util/ToolkitNotification.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/util/ToolkitNotification.java index 7baa096b..1363930b 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/util/ToolkitNotification.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/util/ToolkitNotification.java @@ -19,7 +19,7 @@ import org.eclipse.jface.resource.ImageDescriptor; import org.eclipse.mylyn.commons.ui.dialogs.AbstractNotificationPopup; -public final class ToolkitNotification extends AbstractNotificationPopup { +public class ToolkitNotification extends AbstractNotificationPopup { private final String title; private final String description; @@ -43,6 +43,12 @@ private Image getInfoIcon() { return this.infoIcon; } + /** + * Creates the content area for the notification. + * Subclasses may override this method to customize the notification content. + * + * @param parent the parent composite + */ @Override protected void createContentArea(final Composite parent) { Composite container = new Composite(parent, SWT.NONE); @@ -59,12 +65,12 @@ protected void createContentArea(final Composite parent) { } @Override - protected String getPopupShellTitle() { + protected final String getPopupShellTitle() { return this.title; } @Override - protected void initializeBounds() { + protected final void initializeBounds() { Rectangle clArea = getPrimaryClientArea(); Point initialSize = getShell().computeSize(SWT.DEFAULT, SWT.DEFAULT); int height = Math.max(initialSize.y, MIN_HEIGHT); @@ -105,7 +111,7 @@ private void repositionNotifications() { } @Override - public boolean close() { + public final boolean close() { activeNotifications.remove(this); repositionNotifications(); if (this.infoIcon != null && !this.infoIcon.isDisposed()) { diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/util/UpdateUtils.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/util/UpdateUtils.java new file mode 100644 index 00000000..5083e282 --- /dev/null +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/util/UpdateUtils.java @@ -0,0 +1,123 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package software.aws.toolkits.eclipse.amazonq.util; + +import java.io.BufferedReader; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.time.Duration; + +import org.eclipse.mylyn.commons.ui.dialogs.AbstractNotificationPopup; +import org.eclipse.swt.widgets.Display; +import org.osgi.framework.Version; + +import org.tukaani.xz.XZInputStream; + +import software.aws.toolkits.eclipse.amazonq.exception.AmazonQPluginException; +import software.aws.toolkits.eclipse.amazonq.lsp.manager.fetcher.ArtifactUtils; +import software.aws.toolkits.eclipse.amazonq.plugin.Activator; +import software.aws.toolkits.eclipse.amazonq.telemetry.metadata.PluginClientMetadata; + +public final class UpdateUtils { + private static final String REQUEST_URL = "https://amazonq.eclipsetoolkit.amazonwebservices.com/artifacts.xml.xz"; + private static Version mostRecentNotificationVersion; + private static Version remoteVersion; + private static Version localVersion; + private static final UpdateUtils INSTANCE = new UpdateUtils(); + + public static UpdateUtils getInstance() { + return INSTANCE; + } + + private UpdateUtils() { + mostRecentNotificationVersion = Activator.getPluginStore().getObject(Constants.DO_NOT_SHOW_UPDATE_KEY, Version.class); + String localString = PluginClientMetadata.getInstance().getPluginVersion(); + localVersion = ArtifactUtils.parseVersion(localString.substring(0, localString.lastIndexOf("."))); + } + + private boolean newUpdateAvailable() { + //fetch artifact file containing version info from repo + remoteVersion = fetchRemoteArtifactVersion(REQUEST_URL); + + //return early if either version is unavailable + if (remoteVersion == null || localVersion == null) { + return false; + } + + //prompt should show if never previously displayed or remote version is greater + boolean shouldShowNotification = mostRecentNotificationVersion == null || remoteVersionIsGreater(remoteVersion, mostRecentNotificationVersion); + + return remoteVersionIsGreater(remoteVersion, localVersion) && shouldShowNotification; + } + + public void checkForUpdate() { + if (newUpdateAvailable()) { + showNotification(); + } + } + + private Version fetchRemoteArtifactVersion(final String repositoryUrl) { + HttpClient connection = HttpClientFactory.getInstance(); + try { + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(repositoryUrl)) + .timeout(Duration.ofSeconds(5)) + .GET() + .build(); + + HttpResponse response = connection.send(request, + HttpResponse.BodyHandlers.ofInputStream()); + + // handle response codes + if (response.statusCode() != HttpURLConnection.HTTP_OK) { + throw new AmazonQPluginException("HTTP request failed with response code: " + response.statusCode()); + } + + // process XZ content from input stream + try (InputStream inputStream = response.body(); + XZInputStream xzis = new XZInputStream(inputStream); + BufferedReader reader = new BufferedReader(new InputStreamReader(xzis))) { + + String line; + while ((line = reader.readLine()) != null) { + if (line.contains(" { + AbstractNotificationPopup notification = new PersistentToolkitNotification(Display.getCurrent(), + Constants.PLUGIN_UPDATE_NOTIFICATION_TITLE, + String.format(Constants.PLUGIN_UPDATE_NOTIFICATION_BODY, remoteVersion.toString()), + (selected) -> { + if (selected) { + Activator.getPluginStore().putObject(Constants.DO_NOT_SHOW_UPDATE_KEY, remoteVersion); + } else { + Activator.getPluginStore().remove(Constants.DO_NOT_SHOW_UPDATE_KEY); + } + }); + notification.open(); + }); + } + + private static boolean remoteVersionIsGreater(final Version remote, final Version local) { + return remote.compareTo(local) > 0; + } +}