Skip to content

Commit

Permalink
Merge branch 'main' into breedloj/semanticVersioning
Browse files Browse the repository at this point in the history
  • Loading branch information
breedloj authored Jan 24, 2025
2 parents 8fee1ac + 7cb28c2 commit ba28fdc
Show file tree
Hide file tree
Showing 6 changed files with 238 additions and 4 deletions.
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;
}
}

0 comments on commit ba28fdc

Please sign in to comment.