When using extensions in an embedded browser, you may want to package and distribute them within your app and silently install them.
In this tutorial, I’ll demonstrate how to programmatically install extensions from CRX files, keep them up-to-date, and use them.
Additionally, I’ll show you how to get a CRX file of an extension published in the Chrome Web Store.
Do you want to give end users access to Chrome Web Store instead? We cover that in Chrome extensions in JxBrowser.
Extension
I have chosen the React DevTools extension for this tutorial. It’s not useful for end users, but it fits the purpose of this tutorial and comes in handy during development. Many JavaScript libraries provide dev tools as extensions, which ultimately do the same: extend Chrome DevTools with library-specific features.
With JxBrowser, you can build a user interface with React, and having the React DevTools inside JxBrowser is very neat. This is especially true if an embedded web app needs JxBrowser to communicate with the Java host app, so it can’t run in a standalone browser.
You can build this extension from the source code or get it from the Chrome Web Store as we describe further in the tutorial.
Creating an app
Let’s start by creating a simple app. It will show a browser and a button that opens the standard Chrome DevTools.
import static java.awt.BorderLayout.CENTER;
import static java.awt.BorderLayout.NORTH;
import static javax.swing.BoxLayout.X_AXIS;
import static javax.swing.SwingUtilities.invokeLater;
import static javax.swing.WindowConstants.DISPOSE_ON_CLOSE;
import com.teamdev.jxbrowser.browser.Browser;
import com.teamdev.jxbrowser.engine.Engine;
import com.teamdev.jxbrowser.engine.EngineOptions;
import com.teamdev.jxbrowser.engine.RenderingMode;
import com.teamdev.jxbrowser.view.swing.BrowserView;
import java.awt.Dimension;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.nio.file.Paths;
import javax.swing.BoxLayout;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;
public final class CrxExtensions {
private static final Dimension BUTTON_SIZE = new Dimension(32, 32);
public static void main(String[] args) {
var options = EngineOptions
.newBuilder(RenderingMode.HARDWARE_ACCELERATED)
.userDataDir(Paths.get("<path to the directory>"))
.build();
var engine = Engine.newInstance(options);
var profile = engine.profiles().defaultProfile();
var browser = profile.newBrowser();
invokeLater(() -> {
var frame = new JFrame("CRX Chrome extension in JxBrowser");
frame.addWindowListener(new WindowAdapter() {
@Override
public void windowClosing(WindowEvent e) {
engine.close();
}
});
frame.setDefaultCloseOperation(DISPOSE_ON_CLOSE);
frame.add(BrowserView.newInstance(browser), CENTER);
frame.add(createExtensionBar(browser), NORTH);
frame.setSize(1280, 900);
frame.setLocationRelativeTo(null);
frame.setVisible(true);
browser.navigation().loadUrl("https://react.dev/");
});
}
private static JPanel createExtensionBar(Browser browser) {
var extensionBar = new JPanel();
extensionBar.setLayout(new BoxLayout(extensionBar, X_AXIS));
var openDevTools = new JButton("Open DevTools");
openDevTools.addActionListener(e -> browser.devTools().show());
extensionBar.add(openDevTools);
return extensionBar;
}
}
Note that I highlighted the line that configures the user data directory. This is where Chromium will store the installed extensions. We want this directory to be persistent so the extensions’ state is preserved between launches.
Installing extensions
To install the extension, we need a path to a CRX file. We will locate the available extensions in the resources and pass them on to JxBrowser for installation:
private static final List<String> EXTENSION_FILES = List.of(
"react_dev_tools.crx"
);
public static void main(String[] args) {
...
var profile = engine.profiles().defaultProfile();
var browser = profile.newBrowser();
var extensions = profile.extensions();
for (var crx : EXTENSION_FILES) {
extensions.install(getResourcePath(crx));
}
invokeLater(() -> {
...
});
}
private static Path getResourcePath(String name) {
try {
var resource = CrxExtensions.class.getClassLoader()
.getResource(name);
if (resource != null) {
return Paths.get(resource.toURI());
} else {
throw new IllegalStateException(
"Couldn't find the bundled extension.");
}
} catch (URISyntaxException e) {
throw new IllegalStateException(e);
}
}
In the code above, we install extensions on every app launch. That ensures extensions stay up-to-date: JxBrowser will reinstall an extension if a CRX file has a different version.
Permission to open new tabs
Extensions can open new tabs with arbitrary web content. For example, they can open the settings page or an authorization form. In JxBrowser, we call them extension pop-ups and disable them by default. Now is a good time to enable them:
var extensions = profile.extensions();
for (var crx : EXTENSION_FILES) {
extensions.install(getResourcePath(crx));
extension.set(OpenExtensionPopupCallback.class,
new DefaultOpenExtensionPopupCallback());
}
Interacting with extensions
Most extensions add an icon to the Google Chrome toolbar. This icon is called an extension action, and when clicked, it executes logic or opens a pop-up window.
Let’s have a similar extension bar and add extension actions to the app UI:
import com.teamdev.jxbrowser.extensions.callback.OpenExtensionPopupCallback;
import com.teamdev.jxbrowser.view.swing.callback.DefaultOpenExtensionPopupCallback;
...
private static JPanel createExtensionBar(Browser browser) {
...
var extensions = browser.profile().extensions();
for (var extension : extensions.list()) {
extension.action(browser).ifPresent(action -> {
var button = new JButton();
extensionBar.add(button);
configureActionButton(button, action);
});
}
return extensionBar;
}
Extension actions can change at any time. For example, React DevTools’ action becomes disabled for pages without React. Other extensions can change the icon or a badge in reaction to what is happening in the browser. Let’s keep track of these changes and update the UI:
import com.teamdev.jxbrowser.extensions.callback.OpenExtensionPopupCallback;
import com.teamdev.jxbrowser.extensions.event.ExtensionActionUpdated;
import com.teamdev.jxbrowser.view.swing.callback.DefaultOpenExtensionPopupCallback;
...
private static JPanel createExtensionBar(Browser browser) {
...
var extensions = browser.profile().extensions();
for (var extension : extensions.list()) {
extension.action(browser).ifPresent(action -> {
var button = new JButton();
extensionBar.add(button);
configureActionButton(button, action);
action.on(ExtensionActionUpdated.class, params -> {
invokeLater(() -> configureActionButton(button, action));
});
});
}
return extensionBar;
}
Now, let’s implement the method that displays the icon and name of the action and configures the click handler:
import com.teamdev.jxbrowser.extensions.ExtensionAction;
import com.teamdev.jxbrowser.view.swing.graphics.BitmapImage;
...
private static void configureActionButton(JButton button,
ExtensionAction action) {
var icon = BitmapImage.toToolkit(action.icon());
button.setPreferredSize(new Dimension(32, 32));
button.setText(action.tooltip());
button.setIcon(new ImageIcon(icon));
button.setEnabled(action.isEnabled());
if (button.getActionListeners().length == 0) {
button.addActionListener(e -> action.click());
}
}
Result
In this video, you will see the React DevTools extension installed and used inside JxBrowser.
Getting CRX files from Chrome Web Store
Quite often, open-source extensions publish CRX files on GitHub or provide instructions on how to build them yourself. But if neither the CRX file nor instructions are available, you can get the file from the Chrome Web Store.
To get the extension file from the Chrome Web Store in JxBrowser, register
InstallExtensionCallback
as shown below, navigate to the Chrome Web Store, and
install the extension manually.
Once you install the extension, the callback will copy the downloaded CRX file to the current directory and open it in the file explorer.
import static java.awt.Desktop.getDesktop;
import static javax.swing.SwingUtilities.invokeLater;
import static javax.swing.WindowConstants.DISPOSE_ON_CLOSE;
import com.teamdev.jxbrowser.engine.Engine;
import com.teamdev.jxbrowser.engine.RenderingMode;
import com.teamdev.jxbrowser.extensions.callback.InstallExtensionCallback;
import com.teamdev.jxbrowser.view.swing.BrowserView;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import javax.swing.JFrame;
public final class CrxFileFromChromeWebStore {
private static final String EXTENSION_URL =
"https://chromewebstore.google.com/detail/...";
public static void main(String[] args) {
var engine = Engine.newInstance(RenderingMode.HARDWARE_ACCELERATED);
var browser = engine.newBrowser();
var extensions = browser.profile().extensions();
extensions.set(InstallExtensionCallback.class, (params, tell) -> {
var name = params.extensionName();
var source = Paths.get(params.extensionCrxFile());
var target = Paths.get(name + ".crx").toAbsolutePath();
try {
Files.copy(source, target);
getDesktop().open(target.getParent().toFile());
} catch (IOException e) {
throw new RuntimeException(e);
}
tell.cancel();
});
browser.navigation().loadUrl(EXTENSION_URL);
invokeLater(() -> {
var view = BrowserView.newInstance(browser);
var frame = new JFrame("Chrome Web Store");
frame.add(view);
frame.setSize(1280, 900);
frame.setLocationRelativeTo(null);
frame.addWindowListener(new WindowAdapter() {
@Override
public void windowClosing(WindowEvent e) {
engine.close();
}
});
frame.setDefaultCloseOperation(DISPOSE_ON_CLOSE);
frame.setVisible(true);
});
}
}
Sending…
Sorry, the sending was interrupted
Please try again. If the issue persists, contact us at info@teamdev.com.
Your personal JxBrowser trial key and quick start guide will arrive in your Email Inbox in a few minutes.
Source code
You can find the source code for this example in the JxBrowser-Examples repository.