Skip to content
Closed
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
30 changes: 30 additions & 0 deletions gui/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# APKEditor GUI wrapper

This is a Java Swing wrapper for the CLI commands described in `../cli comands`.
It supports Build (`b`), Decompile (`d`), Info (`info`), Merge (`m`), Protect (`p`),
and Refactor (`x`). The CLI JAR is selected in the UI, so the wrapper does not
bundle or assume a particular APKEditor version.

## Build

Requires Java 8 or newer. A JDK is needed to build the project.

```sh
./build.sh
```

The result is `APKEditor-GUI.jar`.

## Run

```sh
java -jar APKEditor-GUI.jar
```

Run this command on a desktop session. A remote Codespace or SSH terminal without
an X11/Wayland display cannot show a Swing window.

The interface uses Java's system look-and-feel, which gives native controls on
each supported desktop OS while keeping the application in one OS-independent JAR.
The target CLI JAR still needs to be downloaded separately and must be run with a
compatible Java runtime.
12 changes: 12 additions & 0 deletions gui/build.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
#!/usr/bin/env sh
set -eu

ROOT=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
BUILD="$ROOT/build"
rm -rf "$BUILD"
mkdir -p "$BUILD/classes"

find "$ROOT/src" -name '*.java' -print0 | xargs -0 javac -source 8 -target 8 -d "$BUILD/classes"
printf 'Main-Class: com.apkeditor.gui.Main\n' > "$BUILD/manifest.mf"
jar cfm "$ROOT/APKEditor-GUI.jar" "$BUILD/manifest.mf" -C "$BUILD/classes" .
printf 'Created %s\n' "$ROOT/APKEditor-GUI.jar"
Binary file not shown.
Binary file not shown.
Binary file added gui/build/classes/com/apkeditor/gui/CliCommand.class
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file added gui/build/classes/com/apkeditor/gui/Main.class
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
1 change: 1 addition & 0 deletions gui/build/manifest.mf
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Main-Class: com.apkeditor.gui.Main
23 changes: 23 additions & 0 deletions gui/src/com/apkeditor/gui/ArgumentParser.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.apkeditor.gui;

import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public final class ArgumentParser {
private static final Pattern ARGUMENT_PATTERN = Pattern
.compile("(?:\\\"([^\\\"]*)\\\"|'([^']*)'|(\\S+))");

private ArgumentParser() { }

public static List<String> split(String value) {
List<String> arguments = new ArrayList<>();
Matcher matcher = ARGUMENT_PATTERN.matcher(value);
while (matcher.find()) {
arguments.add(matcher.group(1) != null ? matcher.group(1)
: matcher.group(2) != null ? matcher.group(2) : matcher.group(3));
}
return arguments;
}
}
7 changes: 7 additions & 0 deletions gui/src/com/apkeditor/gui/BuildCommand.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.apkeditor.gui;

public final class BuildCommand implements CliCommand {
public String getCode() { return "b"; }
public String getLabel() { return "Build"; }
public String toString() { return getLabel() + " (" + getCode() + ")"; }
}
10 changes: 10 additions & 0 deletions gui/src/com/apkeditor/gui/CliCommand.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.apkeditor.gui;

public interface CliCommand {
String getCode();

String getLabel();

@Override
String toString();
}
44 changes: 44 additions & 0 deletions gui/src/com/apkeditor/gui/CliProcessRunner.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package com.apkeditor.gui;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.List;

public final class CliProcessRunner {
public interface Listener {
void onOutput(String line);
void onFinished(int exitCode);
void onError(String message);
}

public void run(final List<String> command, final Listener listener) {
Thread worker = new Thread(new Runnable() {
@Override
public void run() {
execute(command, listener);
}
}, "cli-process");
worker.start();
}

private void execute(List<String> command, Listener listener) {
try {
Process process = new ProcessBuilder(command).redirectErrorStream(true).start();
try (BufferedReader reader = new BufferedReader(new InputStreamReader(
process.getInputStream(), StandardCharsets.UTF_8))) {
String line;
while ((line = reader.readLine()) != null) {
listener.onOutput(line);
}
}
listener.onFinished(process.waitFor());
} catch (IOException exception) {
listener.onError("Could not start CLI: " + exception.getMessage());
} catch (InterruptedException exception) {
Thread.currentThread().interrupt();
listener.onError("Process interrupted.");
}
}
}
12 changes: 12 additions & 0 deletions gui/src/com/apkeditor/gui/CommandCatalog.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.apkeditor.gui;

public final class CommandCatalog {
private CommandCatalog() { }

public static CliCommand[] all() {
return new CliCommand[] {
new BuildCommand(), new DecompileCommand(), new InfoCommand(),
new MergeCommand(), new ProtectCommand(), new RefactorCommand()
};
}
}
7 changes: 7 additions & 0 deletions gui/src/com/apkeditor/gui/DecompileCommand.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.apkeditor.gui;

public final class DecompileCommand implements CliCommand {
public String getCode() { return "d"; }
public String getLabel() { return "Decompile"; }
public String toString() { return getLabel() + " (" + getCode() + ")"; }
}
19 changes: 19 additions & 0 deletions gui/src/com/apkeditor/gui/FileChooserService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.apkeditor.gui;

import java.awt.Component;
import java.io.File;
import javax.swing.JFileChooser;

public final class FileChooserService {
private FileChooserService() { }

public static File choose(Component parent, int selectionMode, String title) {
JFileChooser chooser = new JFileChooser();
chooser.setDialogTitle(title);
chooser.setFileSelectionMode(selectionMode);
if (chooser.showOpenDialog(parent) == JFileChooser.APPROVE_OPTION) {
return chooser.getSelectedFile();
}
return null;
}
}
7 changes: 7 additions & 0 deletions gui/src/com/apkeditor/gui/InfoCommand.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.apkeditor.gui;

public final class InfoCommand implements CliCommand {
public String getCode() { return "info"; }
public String getLabel() { return "Info"; }
public String toString() { return getLabel() + " (" + getCode() + ")"; }
}
24 changes: 24 additions & 0 deletions gui/src/com/apkeditor/gui/Main.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.apkeditor.gui;

import javax.swing.SwingUtilities;
import javax.swing.UIManager;
import java.awt.GraphicsEnvironment;

public final class Main {
private Main() {
}

public static void main(String[] args) {
if (GraphicsEnvironment.isHeadless()) {
System.err.println("APKEditor GUI cannot open because this Java session has no desktop display. "
+ "Copy the JAR to Windows, macOS, or Linux desktop and run it there.");
return;
}
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (Exception ignored) {
}

SwingUtilities.invokeLater(() -> new MainFrame().setVisible(true));
}
}
170 changes: 170 additions & 0 deletions gui/src/com/apkeditor/gui/MainFrame.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
package com.apkeditor.gui;

import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import javax.swing.BorderFactory;
import javax.swing.JButton;
import javax.swing.JComboBox;
import javax.swing.JFileChooser;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.JTextField;
import javax.swing.SwingUtilities;

public final class MainFrame extends JFrame {
private final JTextField cliJarField = new JTextField();
private final JTextField inputField = new JTextField();
private final JTextField outputField = new JTextField();
private final JTextField extraArgsField = new JTextField();
private final JComboBox<CliCommand> commandBox = new JComboBox<>(CommandCatalog.all());
private final JTextArea outputArea = new JTextArea();
private final JButton runButton = new JButton("Run");
private final CliProcessRunner processRunner = new CliProcessRunner();

public MainFrame() {
super("APKEditor GUI");
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setMinimumSize(new Dimension(760, 520));
setSize(900, 640);
setLocationByPlatform(true);
buildUi();
}

private void buildUi() {
JPanel form = new JPanel(new GridBagLayout());
form.setBorder(BorderFactory.createEmptyBorder(12, 12, 4, 12));
addRow(form, 0, "CLI JAR", cliJarField, "Browse", this::chooseCliJar);
addRow(form, 1, "Command", commandBox, null, null);
addRow(form, 2, "Input", inputField, "Browse", this::chooseInput);
addRow(form, 3, "Output", outputField, "Browse", this::chooseOutput);
addRow(form, 4, "Extra options", extraArgsField, null, null);

JPanel actions = new JPanel(new FlowLayout(FlowLayout.RIGHT));
JButton clearButton = new JButton("Clear output");
clearButton.addActionListener(event -> outputArea.setText(""));
runButton.addActionListener(event -> runCommand());
actions.add(clearButton);
actions.add(runButton);
outputArea.setEditable(false);
outputArea.setLineWrap(true);
outputArea.setWrapStyleWord(true);
outputArea.setBorder(BorderFactory.createEmptyBorder(8, 8, 8, 8));
JScrollPane outputScroll = new JScrollPane(outputArea);
outputScroll.setBorder(BorderFactory.createTitledBorder("CLI output"));

add(form, BorderLayout.NORTH);
add(outputScroll, BorderLayout.CENTER);
add(actions, BorderLayout.SOUTH);
}

private void addRow(JPanel panel, int row, String label, java.awt.Component field,
String buttonLabel, Runnable browseAction) {
GridBagConstraints constraints = new GridBagConstraints();
constraints.gridy = row;
constraints.insets = new Insets(4, 4, 4, 4);
constraints.anchor = GridBagConstraints.LINE_START;
constraints.fill = GridBagConstraints.HORIZONTAL;
constraints.weightx = 0;
panel.add(new JLabel(label), constraints);

constraints.gridx = 1;
constraints.weightx = 1;
panel.add(field, constraints);
if (buttonLabel != null) {
constraints.gridx = 2;
constraints.weightx = 0;
JButton button = new JButton(buttonLabel);
button.addActionListener(event -> browseAction.run());
panel.add(button, constraints);
}
}

private void chooseCliJar() {
choosePath(cliJarField, JFileChooser.FILES_ONLY, "Select CLI JAR");
}

private void chooseInput() {
choosePath(inputField, JFileChooser.FILES_AND_DIRECTORIES, "Select input");
}

private void chooseOutput() {
choosePath(outputField, JFileChooser.FILES_AND_DIRECTORIES, "Select output");
}

private void choosePath(JTextField target, int selectionMode, String title) {
File selectedFile = FileChooserService.choose(this, selectionMode, title);
if (selectedFile != null) {
target.setText(selectedFile.getAbsolutePath());
}
}

private void runCommand() {
String cliJar = cliJarField.getText().trim();
String input = inputField.getText().trim();
if (cliJar.isEmpty() || input.isEmpty()) {
JOptionPane.showMessageDialog(this, "CLI JAR and input are required.",
"Missing input", JOptionPane.WARNING_MESSAGE);
return;
}
if (!Files.isRegularFile(Paths.get(cliJar))) {
JOptionPane.showMessageDialog(this, "The selected CLI JAR does not exist.",
"Invalid CLI JAR", JOptionPane.ERROR_MESSAGE);
return;
}

List<String> command = createCommand(cliJar, input);
outputArea.append("$ " + String.join(" ", command) + "\n");
runButton.setEnabled(false);
processRunner.run(command, new CliProcessRunner.Listener() {
public void onOutput(final String line) {
SwingUtilities.invokeLater(() -> outputArea.append(line + "\n"));
}

public void onFinished(final int exitCode) {
SwingUtilities.invokeLater(() -> {
outputArea.append("\nProcess finished with exit code " + exitCode + "\n");
runButton.setEnabled(true);
});
}

public void onError(final String message) {
SwingUtilities.invokeLater(() -> {
outputArea.append(message + "\n");
runButton.setEnabled(true);
});
}
});
}

private List<String> createCommand(String cliJar, String input) {
List<String> command = new ArrayList<>();
command.add(Paths.get(System.getProperty("java.home"), "bin", "java").toString());
command.add("-jar");
command.add(cliJar);
command.add(((CliCommand) commandBox.getSelectedItem()).getCode());
command.add("-i");
command.add(input);
if (!outputField.getText().trim().isEmpty()) {
command.add("-o");
command.add(outputField.getText().trim());
}
String extraOptions = extraArgsField.getText().trim();
if (!extraOptions.isEmpty()) {
command.addAll(ArgumentParser.split(extraOptions));
}
return command;
}
}
7 changes: 7 additions & 0 deletions gui/src/com/apkeditor/gui/MergeCommand.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.apkeditor.gui;

public final class MergeCommand implements CliCommand {
public String getCode() { return "m"; }
public String getLabel() { return "Merge"; }
public String toString() { return getLabel() + " (" + getCode() + ")"; }
}
Loading
Loading