Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add feature to notify users of new plugin version availability #318

Merged
merged 5 commits into from
Jan 23, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions plugin/META-INF/MANIFEST.MF
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Boolean> checkboxCallback;

public PersistentToolkitNotification(final Display display, final String title,
final String description, final Consumer<Boolean> 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());
}
}
});
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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);
Expand All @@ -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);
Expand Down Expand Up @@ -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()) {
Expand Down
123 changes: 123 additions & 0 deletions plugin/src/software/aws/toolkits/eclipse/amazonq/util/UpdateUtils.java
Original file line number Diff line number Diff line change
@@ -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<InputStream> 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("<artifact classifier=\"osgi.bundle\"")) {
int versionStart = line.indexOf("version=\"") + 9;
int versionEnd = line.indexOf("\"", versionStart);
String fullVersion = line.substring(versionStart, versionEnd);
return ArtifactUtils.parseVersion(fullVersion.substring(0, fullVersion.lastIndexOf(".")));
}
}
}

} catch (Exception e) {
Activator.getLogger().error("Error fetching artifact from remote location.", e);
}
return null;
}

private void showNotification() {
Display.getDefault().asyncExec(() -> {
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;
}
Comment on lines +120 to +122
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One side note - #329 adds support for full semantic versioning of artifacts, and a different dependency (Maven Artifact library) for version constructs. Once that is merged you can switch to using those to get the benefits of being able to diff across labeled versions. Depending on when things get merged that can also be a follow up improvement.

}
Loading