diff --git a/.github/workflows/gradle_build.yml b/.github/workflows/gradle_build.yml index 1a220dc4..8067a157 100644 --- a/.github/workflows/gradle_build.yml +++ b/.github/workflows/gradle_build.yml @@ -1,30 +1,137 @@ -name: Build Jar +name: Build Jar and DEB on: push: branches: [ "master" ] pull_request: branches: [ "master" ] + workflow_dispatch: # ---> Para ejecutar desde la interfaz permissions: contents: read jobs: build: - runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - name: Set up JDK 8 + + - name: Set up JDK 21 uses: actions/setup-java@v4 with: - java-version: '8' + java-version: '21' distribution: 'temurin' + - uses: gradle/actions/setup-gradle@v4 + - name: Build with Gradle - run: ./gradlew fatJar - - uses: actions/upload-artifact@v4 + run: ./gradlew fatJar --no-daemon # --> Evitar algunos errores CI + + - name: Install dependencies for DEB packaging + run: | + sudo apt-get update + sudo apt-get install -y dh-make debhelper devscripts + + - name: Create DEB package structure DEBIAN + run: | + ################ DEBIAN ##################### + mkdir -p deb-package-debian/usr/share/apkeditor + mkdir -p deb-package-debian/usr/bin + mkdir -p deb-package-debian/DEBIAN + chmod 755 deb-package-debian + + # Copy the built jar + cp build/libs/*.jar deb-package-debian/usr/share/apkeditor/apkeditor.jar + + # Create launcher script + echo '#!/bin/sh' > deb-package-debian/usr/bin/apkeditor + echo 'java -jar /usr/share/apkeditor/apkeditor.jar "$@"' >> deb-package-debian/usr/bin/apkeditor + chmod +x deb-package-debian/usr/bin/apkeditor + + # Create control file + cat > deb-package-debian/DEBIAN/control < + Description: APK Editor tool + A tool for editing APK files. + EOL + + - name: Create DEB package structure TERMUX + run: | + ############## TERMUX ############### + mkdir -p deb-package-termux/data/data/com.termux/files/usr/share/apkeditor + mkdir -p deb-package-termux/data/data/com.termux/files/usr/bin + mkdir -p deb-package-termux/DEBIAN + chmod 755 deb-package-termux + + # Copy the built jar + cp build/libs/*.jar deb-package-termux/data/data/com.termux/files/usr/share/apkeditor/apkeditor.jar + + # Create launcher script + # + cat > deb-package-termux/data/data/com.termux/files/usr/bin/apkeditor << 'EOF' + #!/data/data/com.termux/files/usr/bin/bash + + JAR_PATH="/data/data/com.termux/files/usr/share/apkeditor/apkeditor.jar" + if [ ! -f "$JAR_PATH" ]; then + echo "Error: I can't find apkeditor.jar en $JAR_PATH" >&2 + exit 1 + fi + + JAVA_OPTS="-Xmx512M " + + exec java -jar $JAVA_OPTS "$JAR_PATH" "$@" + EOF + + + chmod +x deb-package-termux/data/data/com.termux/files/usr/bin/apkeditor + + # Create control file + cat > deb-package-termux/DEBIAN/control < + Description: APK Editor tool + A tool for editing APK files. + EOL + + + - name: Build DEB package + run: | + #### debian #### + dpkg-deb --build deb-package-debian + mkdir -p artifacts + mv deb-package-debian.deb artifacts/apkeditor_1.4-${{ github.run_number }}_all.deb + #### termux #### + dpkg-deb --build deb-package-termux + mkdir -p artifacts1 + mv deb-package-termux.deb artifacts1/apkeditor_1.4-${{ github.run_number }}_all.deb + + + - name: Upload DEBIAN-DEB-APKEDITOR + uses: actions/upload-artifact@v4 + with: + name: DEBIAN-DEB-APKEDITOR + path: artifacts + + - name: Upload TERMUX-DEB-APKEDITOR + uses: actions/upload-artifact@v4 with: - name: APKEditor - path: build/libs + name: TERMUX-DEB-APKEDITOR + path: artifacts1 + + - name: Upload APKEDITOR.jar + uses: actions/upload-artifact@v4 + with: + name: APKEDITOR + path: build/libs + + diff --git a/.github/workflows/gradle_build_beta.yml b/.github/workflows/gradle_build_beta.yml index d8fecad6..6b21e9fb 100644 --- a/.github/workflows/gradle_build_beta.yml +++ b/.github/workflows/gradle_build_beta.yml @@ -16,14 +16,14 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Set up JDK 8 + - name: Set up JDK 21 uses: actions/setup-java@v4 with: - java-version: '8' + java-version: '21' distribution: 'temurin' - uses: gradle/actions/setup-gradle@v4 - name: Build with Gradle - run: ./gradlew betaJar + run: ./gradlew fatJar - uses: actions/upload-artifact@v4 with: name: APKEditor diff --git a/LICENSE b/LICENSE old mode 100755 new mode 100644 diff --git a/README.md b/README.md old mode 100755 new mode 100644 diff --git a/build.gradle b/build.gradle old mode 100755 new mode 100644 index cae105fb..f8869156 --- a/build.gradle +++ b/build.gradle @@ -1,58 +1,61 @@ +plugins { + id 'application' + id 'java' +} -apply plugin: 'java' +group = 'com.reandroid.apkeditor' +version = '1.4.9' -group 'com.reandroid.apkeditor' -version '1.4.9' +application { + mainClass = 'com.reandroid.apkeditor.Main' +} java { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 + // Definir la compatibilidad con Java 21 + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 } -if (JavaVersion.current().isJava8Compatible()) { - allprojects { - tasks.compileJava { - //options.addStringOption('-Xlint:unchecked', '-quiet') - } - } -} repositories { mavenCentral() } dependencies { - //implementation("io.github.reandroid:ARSCLib:+") - compile(files("$rootProject.projectDir/libs/ARSCLib.jar")) - - // built from: https://github.com/REAndroid/smali-lib - compile(files("$rootProject.projectDir/libs/smali.jar")) - - // built from: https://github.com/REAndroid/JCommand - compile(files("$rootProject.projectDir/libs/JCommand.jar")) + implementation(files("$rootProject.projectDir/libs/ARSCLib.jar")) + implementation(files("$rootProject.projectDir/libs/smali.jar")) + implementation(files("$rootProject.projectDir/libs/JCommand.jar")) } processResources { filesMatching('apkeditor.properties') { - expand('version': version) + expand('version': project.version) } } javadoc { exclude('com/reandroid/test/**') } + jar { exclude('com/reandroid/test/**') duplicatesStrategy = DuplicatesStrategy.FAIL + manifest { + attributes 'Main-Class': application.mainClass.get() + } } +// Configuración de FatJar compatible con Gradle 8.x task fatJar(type: Jar) { duplicatesStrategy = DuplicatesStrategy.EXCLUDE + archiveClassifier = 'all' + manifest { - attributes( - 'Main-Class': 'com.reandroid.apkeditor.Main' - ) + attributes 'Main-Class': application.mainClass.get() + } + + from { + configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) } } - from { configurations.compile.collect { it.isDirectory() ? it : zipTree(it) } } with jar } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar old mode 100755 new mode 100644 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties old mode 100755 new mode 100644 index 8a91a6b6..f5da588c --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https://services.gradle.org/distributions/gradle-6.5.1-all.zip +distributionUrl=https://services.gradle.org/distributions/gradle-8.5-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew old mode 100755 new mode 100644 diff --git a/gradlew.bat b/gradlew.bat old mode 100755 new mode 100644 diff --git a/settings.gradle b/settings.gradle old mode 100755 new mode 100644 diff --git a/src/main/java/com/reandroid/apkeditor/CommandExecutor.java b/src/main/java/com/reandroid/apkeditor/CommandExecutor.java index 8b730372..2eed677a 100644 --- a/src/main/java/com/reandroid/apkeditor/CommandExecutor.java +++ b/src/main/java/com/reandroid/apkeditor/CommandExecutor.java @@ -15,6 +15,8 @@ */ package com.reandroid.apkeditor; +import com.reandroid.apkeditor.core.ExecutionContext; + import com.reandroid.apk.APKLogger; import com.reandroid.archive.ZipEntryMap; import com.reandroid.apk.ApkModule; @@ -97,6 +99,7 @@ public void logMessage(String msg) { if(!mEnableLog){ return; } + ExecutionContext.message(mLogTag + msg); Logger.i(mLogTag + msg); } @Override @@ -104,6 +107,7 @@ public void logError(String msg, Throwable tr) { if(!mEnableLog){ return; } + ExecutionContext.error(mLogTag + msg, tr); Logger.e(mLogTag + msg, tr); } @Override @@ -111,6 +115,7 @@ public void logVerbose(String msg) { if(!mEnableLog){ return; } + ExecutionContext.message(mLogTag + msg); Logger.sameLine(mLogTag + msg); } @Override @@ -146,4 +151,3 @@ protected static void removeSignature(ApkModule module){ archive.remove("stamp-cert-sha256"); } } - diff --git a/src/main/java/com/reandroid/apkeditor/common/AndroidManifestHelper.java b/src/main/java/com/reandroid/apkeditor/common/AndroidManifestHelper.java index d8124fea..6e528b2c 100644 --- a/src/main/java/com/reandroid/apkeditor/common/AndroidManifestHelper.java +++ b/src/main/java/com/reandroid/apkeditor/common/AndroidManifestHelper.java @@ -71,8 +71,9 @@ public static void removeAttributeFromManifestByName(AndroidManifestBlock androi } return; } + // Antes hera un valor entero boolean removed = manifestElement.removeAttributesWithName(resourceName); - if (removed && logger != null) { + if (removed ) { logger.logMessage("Removed-attribute : " + resourceName); } } @@ -86,7 +87,7 @@ public static void removeAttributeFromManifestById(AndroidManifestBlock androidM return; } boolean removed = manifestElement.removeAttributesWithId(resourceId); - if (removed && logger != null) { + if (removed ) { logger.logMessage("Removed-attribute : " + HexUtil.toHex8("@0x", resourceId)); } } @@ -108,7 +109,7 @@ public static void removeAttributeFromManifestAndApplication(AndroidManifestBloc ResXmlElement applicationElement = manifestElement.getElement( AndroidManifest.TAG_application); - if(removed && logger != null) { + if(removed ) { logger.logMessage("Attributes on removed: " + HexUtil.toHex8("0x", resourceId) + " (" + nameForLogging + ")"); } @@ -116,7 +117,7 @@ public static void removeAttributeFromManifestAndApplication(AndroidManifestBloc return; } removed = applicationElement.removeAttributesWithId(resourceId); - if(removed && logger != null) { + if(removed ) { logger.logMessage("Attributes on removed: " + HexUtil.toHex8("0x", resourceId) + " (" + nameForLogging + ")"); } diff --git a/src/main/java/com/reandroid/apkeditor/core/ExecutionContext.java b/src/main/java/com/reandroid/apkeditor/core/ExecutionContext.java new file mode 100644 index 00000000..e5b7b4b7 --- /dev/null +++ b/src/main/java/com/reandroid/apkeditor/core/ExecutionContext.java @@ -0,0 +1,42 @@ +package com.reandroid.apkeditor.core; + +/** + * Contexto por hilo para conectar los loggers existentes con la GUI. + * No contiene estado de proyecto ni estado visual. + */ +public final class ExecutionContext { + private static final ThreadLocal CURRENT = new ThreadLocal<>(); + + private ExecutionContext() {} + + public static void setListener(OperationListener listener) { + if (listener == null) { + CURRENT.remove(); + } else { + CURRENT.set(listener); + } + } + + public static OperationListener getListener() { + return CURRENT.get(); + } + + public static void clear() { + CURRENT.remove(); + } + + public static void message(String message) { + OperationListener listener = CURRENT.get(); + if (listener != null) listener.onMessage(message); + } + + public static void warning(String message) { + OperationListener listener = CURRENT.get(); + if (listener != null) listener.onWarning(message); + } + + public static void error(String message, Throwable error) { + OperationListener listener = CURRENT.get(); + if (listener != null) listener.onError(message, error); + } +} diff --git a/src/main/java/com/reandroid/apkeditor/core/LegacyOptionsOperation.java b/src/main/java/com/reandroid/apkeditor/core/LegacyOptionsOperation.java new file mode 100644 index 00000000..01ee752e --- /dev/null +++ b/src/main/java/com/reandroid/apkeditor/core/LegacyOptionsOperation.java @@ -0,0 +1,50 @@ +package com.reandroid.apkeditor.core; + +import com.reandroid.apkeditor.Options; + +/** + * Adaptador que permite ejecutar el motor existente sin acoplarlo a Swing. + */ +public class LegacyOptionsOperation implements Operation { + private final String id; + private final String name; + private final Options options; + + public LegacyOptionsOperation(String id, String name, Options options) { + this.id = id; + this.name = name; + this.options = options; + } + + @Override + public String getId() { return id; } + + @Override + public String getName() { return name; } + + public Options getOptions() { return options; } + + @Override + public OperationResult execute(OperationListener listener) throws Exception { + long start = System.currentTimeMillis(); + if (listener != null) listener.onStarted(id, name); + + ExecutionContext.setListener(listener); + try { + options.validate(); + options.runCommand(); + OperationResult result = OperationResult.success( + id, System.currentTimeMillis() - start); + if (listener != null) listener.onCompleted(result); + return result; + } catch (Exception ex) { + if (listener != null) listener.onError(ex.getMessage(), ex); + OperationResult result = OperationResult.failure( + id, System.currentTimeMillis() - start, ex); + if (listener != null) listener.onCompleted(result); + throw ex; + } finally { + ExecutionContext.clear(); + } + } +} diff --git a/src/main/java/com/reandroid/apkeditor/core/Operation.java b/src/main/java/com/reandroid/apkeditor/core/Operation.java new file mode 100644 index 00000000..055ecff3 --- /dev/null +++ b/src/main/java/com/reandroid/apkeditor/core/Operation.java @@ -0,0 +1,11 @@ +package com.reandroid.apkeditor.core; + +/** + * Unidad de trabajo independiente de la interfaz. + * CLI y GUI pueden ejecutar las mismas operaciones. + */ +public interface Operation { + String getId(); + String getName(); + OperationResult execute(OperationListener listener) throws Exception; +} diff --git a/src/main/java/com/reandroid/apkeditor/core/OperationListener.java b/src/main/java/com/reandroid/apkeditor/core/OperationListener.java new file mode 100644 index 00000000..f8035584 --- /dev/null +++ b/src/main/java/com/reandroid/apkeditor/core/OperationListener.java @@ -0,0 +1,10 @@ +package com.reandroid.apkeditor.core; + +public interface OperationListener { + default void onStarted(String operationId, String operationName) {} + default void onProgress(int percent) {} + default void onMessage(String message) {} + default void onWarning(String message) {} + default void onError(String message, Throwable error) {} + default void onCompleted(OperationResult result) {} +} diff --git a/src/main/java/com/reandroid/apkeditor/core/OperationRegistry.java b/src/main/java/com/reandroid/apkeditor/core/OperationRegistry.java new file mode 100644 index 00000000..2a4b5c10 --- /dev/null +++ b/src/main/java/com/reandroid/apkeditor/core/OperationRegistry.java @@ -0,0 +1,21 @@ +package com.reandroid.apkeditor.core; + +import java.util.*; + +public final class OperationRegistry { + private final Map operations = new LinkedHashMap<>(); + + public OperationRegistry register(Operation operation) { + Objects.requireNonNull(operation, "operation"); + operations.put(operation.getId(), operation); + return this; + } + + public Operation get(String id) { + return operations.get(id); + } + + public Collection all() { + return Collections.unmodifiableCollection(operations.values()); + } +} diff --git a/src/main/java/com/reandroid/apkeditor/core/OperationResult.java b/src/main/java/com/reandroid/apkeditor/core/OperationResult.java new file mode 100644 index 00000000..f5dd2600 --- /dev/null +++ b/src/main/java/com/reandroid/apkeditor/core/OperationResult.java @@ -0,0 +1,29 @@ +package com.reandroid.apkeditor.core; + +public final class OperationResult { + private final boolean success; + private final String operationId; + private final long durationMillis; + private final Throwable error; + + private OperationResult(boolean success, String operationId, + long durationMillis, Throwable error) { + this.success = success; + this.operationId = operationId; + this.durationMillis = durationMillis; + this.error = error; + } + + public static OperationResult success(String operationId, long durationMillis) { + return new OperationResult(true, operationId, durationMillis, null); + } + + public static OperationResult failure(String operationId, long durationMillis, Throwable error) { + return new OperationResult(false, operationId, durationMillis, error); + } + + public boolean isSuccess() { return success; } + public String getOperationId() { return operationId; } + public long getDurationMillis() { return durationMillis; } + public Throwable getError() { return error; } +} diff --git a/src/main/java/com/reandroid/apkeditor/gui/GUIApplication.java b/src/main/java/com/reandroid/apkeditor/gui/GUIApplication.java new file mode 100644 index 00000000..afd5cb95 --- /dev/null +++ b/src/main/java/com/reandroid/apkeditor/gui/GUIApplication.java @@ -0,0 +1,14 @@ +package com.reandroid.apkeditor.gui; + +import javax.swing.SwingUtilities; + +public final class GUIApplication { + private GUIApplication() {} + + public static void main(String[] args) { + SwingUtilities.invokeLater(() -> { + MainWindow window = new MainWindow(); + window.setVisible(true); + }); + } +} diff --git a/src/main/java/com/reandroid/apkeditor/gui/MainWindow.java b/src/main/java/com/reandroid/apkeditor/gui/MainWindow.java new file mode 100644 index 00000000..0521cf1d --- /dev/null +++ b/src/main/java/com/reandroid/apkeditor/gui/MainWindow.java @@ -0,0 +1,144 @@ +package com.reandroid.apkeditor.gui; + +import com.reandroid.apkeditor.core.OperationRegistry; +import com.reandroid.apkeditor.gui.components.*; +import com.reandroid.apkeditor.gui.navigation.*; +import com.reandroid.apkeditor.gui.theme.*; +import com.reandroid.apkeditor.gui.view.*; +import com.reandroid.apkeditor.operations.OperationCatalog; + +import javax.swing.*; +import java.awt.*; +import java.util.LinkedHashMap; +import java.util.Map; + +public final class MainWindow extends JFrame { + private final OperationRegistry registry; + private final CardLayout cards = new CardLayout(); + private final JPanel content = Theme.panel(cards); + private final Navigator navigator = new Navigator(content, cards); + private final LogConsole logs = new LogConsole(); + private final ProgressPanel progress = new ProgressPanel(); + private final JLabel status = new JLabel("● READY"); + + public MainWindow() { + super("APKEditor · Futuristic"); + Theme.install(); + registry = OperationCatalog.create(); + + setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE); + setMinimumSize(new Dimension(1100, 720)); + setSize(1280, 800); + setLocationRelativeTo(null); + + JPanel root = Theme.panel(new BorderLayout()); + root.add(createTopBar(), BorderLayout.NORTH); + root.add(createCenter(), BorderLayout.CENTER); + root.add(createStatusBar(), BorderLayout.SOUTH); + setContentPane(root); + } + + private JComponent createTopBar() { + JPanel bar = Theme.panel(new BorderLayout()); + bar.setBorder(BorderFactory.createCompoundBorder( + BorderFactory.createMatteBorder(0, 0, 1, 0, Colors.BORDER), + Theme.empty(14))); + + JLabel brand = new JLabel("◈ APKEDITOR"); + brand.setFont(Fonts.SECTION); + brand.setForeground(Colors.PRIMARY); + + JLabel engine = new JLabel("CORE ENGINE"); + engine.setFont(Fonts.SMALL); + engine.setForeground(Colors.TEXT_MUTED); + + bar.add(brand, BorderLayout.WEST); + bar.add(engine, BorderLayout.EAST); + return bar; + } + + private JComponent createCenter() { + JPanel center = Theme.panel(new BorderLayout()); + center.add(createSidebar(), BorderLayout.WEST); + buildViews(); + center.add(content, BorderLayout.CENTER); + return center; + } + + private JComponent createSidebar() { + JPanel side = Theme.panel(new GridLayout(0, 1, 0, 6)); + side.setPreferredSize(new Dimension(190, 0)); + side.setBorder(BorderFactory.createCompoundBorder( + BorderFactory.createMatteBorder(0, 0, 0, 1, Colors.BORDER), + Theme.empty(14))); + + addNav(side, "DASHBOARD", Route.DASHBOARD); + addNav(side, "DECODE", Route.DECODE); + addNav(side, "BUILD", Route.BUILD); + addNav(side, "MERGE", Route.MERGE); + addNav(side, "REFACTOR", Route.REFACTOR); + addNav(side, "PROTECT", Route.PROTECT); + addNav(side, "INFO", Route.INFO); + addNav(side, "LOGS", Route.LOGS); + addNav(side, "SETTINGS", Route.SETTINGS); + return side; + } + + private void addNav(JPanel parent, String text, Route route) { + NeonButton button = new NeonButton(text); + button.setHorizontalAlignment(SwingConstants.LEFT); + button.addActionListener(e -> navigator.show(route)); + parent.add(button); + } + + private void buildViews() { + content.add(new DashboardView(id -> { + try { + navigator.show(Route.valueOf(id.toUpperCase())); + } catch (IllegalArgumentException ignored) {} + }), Route.DASHBOARD.name()); + + Map routes = new LinkedHashMap<>(); + routes.put("decode", Route.DECODE); + routes.put("build", Route.BUILD); + routes.put("merge", Route.MERGE); + routes.put("refactor", Route.REFACTOR); + routes.put("protect", Route.PROTECT); + routes.put("info", Route.INFO); + + for (var entry : routes.entrySet()) { + var operation = registry.get(entry.getKey()); + content.add(new OperationView( + entry.getKey(), operation, logs, progress, + busy -> status.setText(busy ? "● RUNNING" : "● READY")), + entry.getValue().name()); + } + + content.add(new LogsView(logs), Route.LOGS.name()); + + JPanel settings = Theme.panel(new BorderLayout()); + settings.setBorder(Theme.empty(24)); + JLabel title = new JLabel("SETTINGS"); + title.setFont(Fonts.TITLE); + title.setForeground(Colors.TEXT); + JLabel info = new JLabel("Theme, paths and engine preferences can be moved here later.
" + + "The important rule is: settings must not leak into operation code."); + info.setForeground(Colors.TEXT_MUTED); + settings.add(title, BorderLayout.NORTH); + settings.add(info, BorderLayout.CENTER); + content.add(settings, Route.SETTINGS.name()); + + navigator.show(Route.DASHBOARD); + } + + private JComponent createStatusBar() { + JPanel bottom = Theme.panel(new BorderLayout(10, 0)); + bottom.setBorder(BorderFactory.createCompoundBorder( + BorderFactory.createMatteBorder(1, 0, 0, 0, Colors.BORDER), + Theme.empty(8))); + status.setForeground(Colors.SUCCESS); + bottom.add(status, BorderLayout.WEST); + bottom.add(progress, BorderLayout.CENTER); + return bottom; + } +} diff --git a/src/main/java/com/reandroid/apkeditor/gui/components/FileSelector.java b/src/main/java/com/reandroid/apkeditor/gui/components/FileSelector.java new file mode 100644 index 00000000..c473da4f --- /dev/null +++ b/src/main/java/com/reandroid/apkeditor/gui/components/FileSelector.java @@ -0,0 +1,54 @@ +package com.reandroid.apkeditor.gui.components; + +import com.reandroid.apkeditor.gui.theme.Colors; +import com.reandroid.apkeditor.gui.theme.Fonts; + +import javax.swing.*; +import java.awt.*; +import java.io.File; + +public final class FileSelector extends JPanel { + private final JTextField field = new JTextField(); + + public FileSelector(String label, boolean directory) { + setLayout(new BorderLayout(8, 4)); + setBackground(Colors.BACKGROUND); + + JLabel title = new JLabel(label); + title.setFont(Fonts.SMALL); + title.setForeground(Colors.TEXT_MUTED); + + NeonButton browse = new NeonButton("BROWSE"); + browse.addActionListener(e -> choose(directory)); + + field.setFont(Fonts.BODY); + field.setBorder(BorderFactory.createLineBorder(Colors.BORDER)); + + JPanel center = new JPanel(new BorderLayout(6, 0)); + center.setOpaque(false); + center.add(field, BorderLayout.CENTER); + center.add(browse, BorderLayout.EAST); + + add(title, BorderLayout.NORTH); + add(center, BorderLayout.CENTER); + } + + private void choose(boolean directory) { + JFileChooser chooser = new JFileChooser(); + chooser.setFileSelectionMode(directory + ? JFileChooser.DIRECTORIES_ONLY + : JFileChooser.FILES_ONLY); + if (chooser.showOpenDialog(this) == JFileChooser.APPROVE_OPTION) { + field.setText(chooser.getSelectedFile().getAbsolutePath()); + } + } + + public File getFile() { + String value = field.getText().trim(); + return value.isEmpty() ? null : new File(value); + } + + public void setFile(File file) { + field.setText(file == null ? "" : file.getAbsolutePath()); + } +} diff --git a/src/main/java/com/reandroid/apkeditor/gui/components/LogConsole.java b/src/main/java/com/reandroid/apkeditor/gui/components/LogConsole.java new file mode 100644 index 00000000..8ce9ba8c --- /dev/null +++ b/src/main/java/com/reandroid/apkeditor/gui/components/LogConsole.java @@ -0,0 +1,37 @@ +package com.reandroid.apkeditor.gui.components; + +import com.reandroid.apkeditor.gui.theme.Colors; +import com.reandroid.apkeditor.gui.theme.Fonts; + +import javax.swing.*; +import java.awt.*; + +public final class LogConsole extends JPanel { + private final JTextArea area = new JTextArea(); + + public LogConsole() { + setLayout(new BorderLayout()); + setBackground(Colors.SURFACE); + setBorder(BorderFactory.createLineBorder(Colors.BORDER)); + + area.setEditable(false); + area.setFont(Fonts.MONO); + area.setBackground(Colors.SURFACE); + area.setForeground(Colors.TEXT); + area.setLineWrap(true); + area.setWrapStyleWord(true); + + add(new JScrollPane(area), BorderLayout.CENTER); + } + + public void append(String message) { + SwingUtilities.invokeLater(() -> { + area.append(message + System.lineSeparator()); + area.setCaretPosition(area.getDocument().getLength()); + }); + } + + public void clear() { + area.setText(""); + } +} diff --git a/src/main/java/com/reandroid/apkeditor/gui/components/NeonButton.java b/src/main/java/com/reandroid/apkeditor/gui/components/NeonButton.java new file mode 100644 index 00000000..6c5ac7e1 --- /dev/null +++ b/src/main/java/com/reandroid/apkeditor/gui/components/NeonButton.java @@ -0,0 +1,25 @@ +package com.reandroid.apkeditor.gui.components; + +import com.reandroid.apkeditor.gui.theme.Colors; +import com.reandroid.apkeditor.gui.theme.Fonts; + +import javax.swing.*; +import java.awt.*; + +public class NeonButton extends JButton { + public NeonButton(String text) { + super(text); + setFont(Fonts.BODY); + setForeground(Colors.TEXT); + setBackground(Colors.SURFACE_2); + setFocusPainted(false); + setBorder(BorderFactory.createLineBorder(Colors.BORDER)); + setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); + setMargin(new Insets(10, 14, 10, 14)); + } + + public void setAccent(boolean accent) { + setForeground(accent ? Colors.BACKGROUND : Colors.TEXT); + setBackground(accent ? Colors.PRIMARY : Colors.SURFACE_2); + } +} diff --git a/src/main/java/com/reandroid/apkeditor/gui/components/ProgressPanel.java b/src/main/java/com/reandroid/apkeditor/gui/components/ProgressPanel.java new file mode 100644 index 00000000..e633846d --- /dev/null +++ b/src/main/java/com/reandroid/apkeditor/gui/components/ProgressPanel.java @@ -0,0 +1,32 @@ +package com.reandroid.apkeditor.gui.components; + +import com.reandroid.apkeditor.gui.theme.Colors; + +import javax.swing.*; +import java.awt.*; + +public final class ProgressPanel extends JPanel { + private final JLabel status = new JLabel("READY"); + private final JProgressBar progress = new JProgressBar(); + + public ProgressPanel() { + setLayout(new BorderLayout(10, 0)); + setBackground(Colors.SURFACE); + setBorder(BorderFactory.createEmptyBorder(8, 10, 8, 10)); + status.setForeground(Colors.TEXT); + progress.setIndeterminate(false); + add(status, BorderLayout.WEST); + add(progress, BorderLayout.CENTER); + } + + public void start(String text) { + status.setText(text); + progress.setIndeterminate(true); + } + + public void complete(boolean ok) { + progress.setIndeterminate(false); + progress.setValue(100); + status.setText(ok ? "DONE" : "FAILED"); + } +} diff --git a/src/main/java/com/reandroid/apkeditor/gui/model/AppState.java b/src/main/java/com/reandroid/apkeditor/gui/model/AppState.java new file mode 100644 index 00000000..0216391e --- /dev/null +++ b/src/main/java/com/reandroid/apkeditor/gui/model/AppState.java @@ -0,0 +1,22 @@ +package com.reandroid.apkeditor.gui.model; + +import java.io.File; + +public final class AppState { + private File currentInput; + private File currentOutput; + private String activeRoute = "dashboard"; + private boolean busy; + + public File getCurrentInput() { return currentInput; } + public void setCurrentInput(File currentInput) { this.currentInput = currentInput; } + + public File getCurrentOutput() { return currentOutput; } + public void setCurrentOutput(File currentOutput) { this.currentOutput = currentOutput; } + + public String getActiveRoute() { return activeRoute; } + public void setActiveRoute(String activeRoute) { this.activeRoute = activeRoute; } + + public boolean isBusy() { return busy; } + public void setBusy(boolean busy) { this.busy = busy; } +} diff --git a/src/main/java/com/reandroid/apkeditor/gui/navigation/Navigator.java b/src/main/java/com/reandroid/apkeditor/gui/navigation/Navigator.java new file mode 100644 index 00000000..60052506 --- /dev/null +++ b/src/main/java/com/reandroid/apkeditor/gui/navigation/Navigator.java @@ -0,0 +1,18 @@ +package com.reandroid.apkeditor.gui.navigation; + +import java.awt.CardLayout; +import java.awt.Container; + +public final class Navigator { + private final Container container; + private final CardLayout layout; + + public Navigator(Container container, CardLayout layout) { + this.container = container; + this.layout = layout; + } + + public void show(Route route) { + layout.show(container, route.name()); + } +} diff --git a/src/main/java/com/reandroid/apkeditor/gui/navigation/Route.java b/src/main/java/com/reandroid/apkeditor/gui/navigation/Route.java new file mode 100644 index 00000000..26192063 --- /dev/null +++ b/src/main/java/com/reandroid/apkeditor/gui/navigation/Route.java @@ -0,0 +1,13 @@ +package com.reandroid.apkeditor.gui.navigation; + +public enum Route { + DASHBOARD, + DECODE, + BUILD, + MERGE, + REFACTOR, + PROTECT, + INFO, + LOGS, + SETTINGS +} diff --git a/src/main/java/com/reandroid/apkeditor/gui/theme/Colors.java b/src/main/java/com/reandroid/apkeditor/gui/theme/Colors.java new file mode 100644 index 00000000..f28fb036 --- /dev/null +++ b/src/main/java/com/reandroid/apkeditor/gui/theme/Colors.java @@ -0,0 +1,21 @@ +package com.reandroid.apkeditor.gui.theme; + +import java.awt.Color; + +public final class Colors { + private Colors() {} + + public static final Color BACKGROUND = new Color(8, 11, 18); + public static final Color SURFACE = new Color(15, 20, 30); + public static final Color SURFACE_2 = new Color(21, 28, 41); + public static final Color BORDER = new Color(39, 54, 74); + + public static final Color PRIMARY = new Color(40, 210, 255); + public static final Color SECONDARY = new Color(139, 92, 246); + public static final Color SUCCESS = new Color(52, 211, 153); + public static final Color WARNING = new Color(251, 191, 36); + public static final Color ERROR = new Color(248, 113, 113); + + public static final Color TEXT = new Color(231, 238, 248); + public static final Color TEXT_MUTED = new Color(139, 153, 174); +} diff --git a/src/main/java/com/reandroid/apkeditor/gui/theme/Fonts.java b/src/main/java/com/reandroid/apkeditor/gui/theme/Fonts.java new file mode 100644 index 00000000..709ae8c1 --- /dev/null +++ b/src/main/java/com/reandroid/apkeditor/gui/theme/Fonts.java @@ -0,0 +1,13 @@ +package com.reandroid.apkeditor.gui.theme; + +import java.awt.Font; + +public final class Fonts { + private Fonts() {} + + public static final Font TITLE = new Font("SansSerif", Font.BOLD, 24); + public static final Font SECTION = new Font("SansSerif", Font.BOLD, 15); + public static final Font BODY = new Font("SansSerif", Font.PLAIN, 13); + public static final Font SMALL = new Font("SansSerif", Font.PLAIN, 11); + public static final Font MONO = new Font("Monospaced", Font.PLAIN, 12); +} diff --git a/src/main/java/com/reandroid/apkeditor/gui/theme/Theme.java b/src/main/java/com/reandroid/apkeditor/gui/theme/Theme.java new file mode 100644 index 00000000..adb17d2a --- /dev/null +++ b/src/main/java/com/reandroid/apkeditor/gui/theme/Theme.java @@ -0,0 +1,43 @@ +package com.reandroid.apkeditor.gui.theme; + +import javax.swing.*; +import javax.swing.border.Border; +import javax.swing.border.EmptyBorder; +import java.awt.*; + +public final class Theme { + private Theme() { + } + + public static void install() { + UIManager.put("Panel.background", Colors.BACKGROUND); + UIManager.put("Label.foreground", Colors.TEXT); + UIManager.put("TextField.background", Colors.SURFACE_2); + UIManager.put("TextField.foreground", Colors.TEXT); + UIManager.put("TextField.caretForeground", Colors.PRIMARY); + UIManager.put("TextArea.background", Colors.SURFACE); + UIManager.put("TextArea.foreground", Colors.TEXT); + UIManager.put("Button.background", Colors.SURFACE_2); + UIManager.put("Button.foreground", Colors.TEXT); + UIManager.put("Button.font", Fonts.BODY); + UIManager.put("ScrollPane.border", BorderFactory.createEmptyBorder()); + } + + public static JPanel panel(LayoutManager layout) { + JPanel panel = new JPanel(layout); + panel.setBackground(Colors.BACKGROUND); + return panel; + } + + public static Border empty(int size) { + return new EmptyBorder(size, size, size, size); + } + + public static void card(JComponent component) { + component.setOpaque(true); + component.setBackground(Colors.SURFACE); + component.setBorder(BorderFactory.createCompoundBorder( + BorderFactory.createLineBorder(Colors.BORDER), + new EmptyBorder(16, 16, 16, 16))); + } +} diff --git a/src/main/java/com/reandroid/apkeditor/gui/view/DashboardView.java b/src/main/java/com/reandroid/apkeditor/gui/view/DashboardView.java new file mode 100644 index 00000000..6479ff6f --- /dev/null +++ b/src/main/java/com/reandroid/apkeditor/gui/view/DashboardView.java @@ -0,0 +1,60 @@ +package com.reandroid.apkeditor.gui.view; + +import com.reandroid.apkeditor.gui.components.NeonButton; +import com.reandroid.apkeditor.gui.theme.*; + +import javax.swing.*; +import java.awt.*; +import java.util.function.Consumer; + +public final class DashboardView extends JPanel { + public DashboardView(Consumer navigate) { + setLayout(new BorderLayout(0, 20)); + setBackground(Colors.BACKGROUND); + setBorder(Theme.empty(24)); + + JLabel title = new JLabel("APK EDITOR"); + title.setFont(Fonts.TITLE); + title.setForeground(Colors.TEXT); + + JLabel subtitle = new JLabel("Modern control center · core engine ready"); + subtitle.setFont(Fonts.BODY); + subtitle.setForeground(Colors.TEXT_MUTED); + + JPanel heading = Theme.panel(new BorderLayout()); + heading.add(title, BorderLayout.NORTH); + heading.add(subtitle, BorderLayout.SOUTH); + add(heading, BorderLayout.NORTH); + + JPanel grid = Theme.panel(new GridLayout(2, 3, 12, 12)); + addCard(grid, "DECODE", "APK → project", "decode", navigate); + addCard(grid, "BUILD", "project → APK", "build", navigate); + addCard(grid, "MERGE", "merge modules", "merge", navigate); + addCard(grid, "REFACTOR", "rename resources", "refactor", navigate); + addCard(grid, "PROTECT", "protect output", "protect", navigate); + addCard(grid, "INFO", "inspect APK", "info", navigate); + add(grid, BorderLayout.CENTER); + } + + private void addCard(JPanel parent, String title, String desc, + String route, Consumer navigate) { + JPanel card = Theme.panel(new BorderLayout(8, 8)); + Theme.card(card); + + JLabel t = new JLabel(title); + t.setFont(Fonts.SECTION); + t.setForeground(Colors.PRIMARY); + + JLabel d = new JLabel("" + desc + ""); + d.setForeground(Colors.TEXT_MUTED); + + NeonButton open = new NeonButton("OPEN"); + open.setAccent(true); + open.addActionListener(e -> navigate.accept(route)); + + card.add(t, BorderLayout.NORTH); + card.add(d, BorderLayout.CENTER); + card.add(open, BorderLayout.SOUTH); + parent.add(card); + } +} diff --git a/src/main/java/com/reandroid/apkeditor/gui/view/LogsView.java b/src/main/java/com/reandroid/apkeditor/gui/view/LogsView.java new file mode 100644 index 00000000..0a0bc761 --- /dev/null +++ b/src/main/java/com/reandroid/apkeditor/gui/view/LogsView.java @@ -0,0 +1,30 @@ +package com.reandroid.apkeditor.gui.view; + +import com.reandroid.apkeditor.gui.components.LogConsole; +import com.reandroid.apkeditor.gui.components.NeonButton; +import com.reandroid.apkeditor.gui.theme.*; + +import javax.swing.*; +import java.awt.*; + +public final class LogsView extends JPanel { + public LogsView(LogConsole console) { + setLayout(new BorderLayout(0, 10)); + setBackground(Colors.BACKGROUND); + setBorder(Theme.empty(24)); + + JPanel header = Theme.panel(new BorderLayout()); + JLabel title = new JLabel("OPERATION LOG"); + title.setFont(Fonts.TITLE); + title.setForeground(Colors.TEXT); + + NeonButton clear = new NeonButton("CLEAR"); + clear.addActionListener(e -> console.clear()); + + header.add(title, BorderLayout.WEST); + header.add(clear, BorderLayout.EAST); + + add(header, BorderLayout.NORTH); + add(console, BorderLayout.CENTER); + } +} diff --git a/src/main/java/com/reandroid/apkeditor/gui/view/OperationView.java b/src/main/java/com/reandroid/apkeditor/gui/view/OperationView.java new file mode 100644 index 00000000..2505aeca --- /dev/null +++ b/src/main/java/com/reandroid/apkeditor/gui/view/OperationView.java @@ -0,0 +1,123 @@ +package com.reandroid.apkeditor.gui.view; + +import com.reandroid.apkeditor.core.*; +import com.reandroid.apkeditor.gui.components.*; +import com.reandroid.apkeditor.gui.theme.*; + +import javax.swing.*; +import java.awt.*; +import java.io.File; +import java.util.function.Consumer; + +public final class OperationView extends JPanel { + private final String operationId; + private final Operation operation; + private final FileSelector input = new FileSelector("INPUT", false); + private final FileSelector output = new FileSelector("OUTPUT", true); + private final LogConsole logs; + private final ProgressPanel progress; + private final JButton runButton = new NeonButton("RUN OPERATION"); + private final Consumer busyState; + + public OperationView(String operationId, Operation operation, + LogConsole logs, ProgressPanel progress, + Consumer busyState) { + this.operationId = operationId; + this.operation = operation; + this.logs = logs; + this.progress = progress; + this.busyState = busyState; + + setLayout(new BorderLayout(0, 16)); + setBackground(Colors.BACKGROUND); + setBorder(Theme.empty(24)); + + JLabel title = new JLabel(operation.getName().toUpperCase()); + title.setFont(Fonts.TITLE); + title.setForeground(Colors.TEXT); + add(title, BorderLayout.NORTH); + + JPanel form = Theme.panel(new GridBagLayout()); + Theme.card(form); + GridBagConstraints c = new GridBagConstraints(); + c.gridx = 0; c.gridy = 0; c.weightx = 1; c.fill = GridBagConstraints.HORIZONTAL; + c.insets = new Insets(8, 8, 8, 8); + form.add(input, c); + + c.gridy++; + form.add(output, c); + + c.gridy++; + c.anchor = GridBagConstraints.WEST; + runButton.setFont(Fonts.SECTION); + ((NeonButton) runButton).setAccent(true); + runButton.addActionListener(e -> run()); + form.add(runButton, c); + + add(form, BorderLayout.CENTER); + } + + private void run() { + if (!(operation instanceof LegacyOptionsOperation legacy)) { + logs.append("[ERROR] Unsupported operation adapter: " + operationId); + return; + } + + File in = input.getFile(); + File out = output.getFile(); + + if (in == null) { + logs.append("[WARN] Select an input first."); + return; + } + + try { + var options = legacy.getOptions(); + options.inputFile = in; + if (out != null && operationId.equals("decode")) { + options.outputFile = out; + } else if (out != null) { + options.outputFile = out; + } + + busyState.accept(true); + runButton.setEnabled(false); + logs.append("[START] " + operation.getName()); + progress.start(operation.getName()); + + Thread worker = new Thread(() -> { + try { + operation.execute(new OperationListener() { + public void onMessage(String message) { + logs.append("[INFO] " + message); + } + public void onWarning(String message) { + logs.append("[WARN] " + message); + } + public void onError(String message, Throwable error) { + logs.append("[ERROR] " + (message == null ? error : message)); + } + public void onCompleted(OperationResult result) { + SwingUtilities.invokeLater(() -> { + progress.complete(result.isSuccess()); + runButton.setEnabled(true); + busyState.accept(false); + logs.append(result.isSuccess() + ? "[DONE] " + operation.getName() + : "[FAILED] " + operation.getName()); + }); + } + }); + } catch (Exception ignored) { + // onCompleted/onError already report the failure. + } + }, "apkeditor-" + operationId); + worker.setDaemon(true); + worker.start(); + } catch (Exception ex) { + logs.append("[ERROR] " + ex.getMessage()); + runButton.setEnabled(true); + busyState.accept(false); + } + } +} diff --git a/src/main/java/com/reandroid/apkeditor/operations/OperationCatalog.java b/src/main/java/com/reandroid/apkeditor/operations/OperationCatalog.java new file mode 100644 index 00000000..317eb82c --- /dev/null +++ b/src/main/java/com/reandroid/apkeditor/operations/OperationCatalog.java @@ -0,0 +1,28 @@ +package com.reandroid.apkeditor.operations; + +import com.reandroid.apkeditor.core.OperationRegistry; +import com.reandroid.apkeditor.core.LegacyOptionsOperation; +import com.reandroid.apkeditor.compile.BuildOptions; +import com.reandroid.apkeditor.decompile.DecompileOptions; +import com.reandroid.apkeditor.info.InfoOptions; +import com.reandroid.apkeditor.merge.MergerOptions; +import com.reandroid.apkeditor.protect.ProtectorOptions; +import com.reandroid.apkeditor.refactor.RefactorOptions; + +/** + * Punto único para registrar las operaciones que ya existen en el motor. + * Aquí no se dibuja ninguna ventana. + */ +public final class OperationCatalog { + private OperationCatalog() {} + + public static OperationRegistry create() { + return new OperationRegistry() + .register(new LegacyOptionsOperation("decode", "Decode APK", new DecompileOptions())) + .register(new LegacyOptionsOperation("build", "Build APK", new BuildOptions())) + .register(new LegacyOptionsOperation("merge", "Merge APK", new MergerOptions())) + .register(new LegacyOptionsOperation("refactor", "Refactor", new RefactorOptions())) + .register(new LegacyOptionsOperation("protect", "Protect", new ProtectorOptions())) + .register(new LegacyOptionsOperation("info", "APK Info", new InfoOptions())); + } +} diff --git a/src/main/java/com/reandroid/commons/utils/ElapsedTime.java b/src/main/java/com/reandroid/commons/utils/ElapsedTime.java index 7f6b6997..75443bf2 100644 --- a/src/main/java/com/reandroid/commons/utils/ElapsedTime.java +++ b/src/main/java/com/reandroid/commons/utils/ElapsedTime.java @@ -85,4 +85,4 @@ private String msToDisplayTime(long l, boolean includeMs, boolean ignoreZero){ } return String.format(Locale.ENGLISH,"%02d:%02d:%02d", hr, min, sec); } -} \ No newline at end of file +} diff --git a/src/main/resources/apkeditor.properties b/src/main/resources/apkeditor.properties old mode 100755 new mode 100644 diff --git a/src/main/resources/color_names.txt b/src/main/resources/color_names.txt index 6d62e023..6d8716e3 100644 --- a/src/main/resources/color_names.txt +++ b/src/main/resources/color_names.txt @@ -1563,4 +1563,4 @@ #FFE4D69B,Zombie #FFA59B91,Zorba #FF044022,Zuccini -#FFEDF6FF,Zumthor \ No newline at end of file +#FFEDF6FF,Zumthor diff --git a/src/main/resources/protect_dic_dir_name.txt b/src/main/resources/protect_dic_dir_name.txt index 6606bb20..e3c6972b 100644 --- a/src/main/resources/protect_dic_dir_name.txt +++ b/src/main/resources/protect_dic_dir_name.txt @@ -24,4 +24,4 @@ res/values/integers.xml res/values/plurals.xml res/values/public.xml res/values/strings.xml -res/values/styles.xml" \ No newline at end of file +res/values/styles.xml" diff --git a/src/main/resources/protect_dic_file_name.txt b/src/main/resources/protect_dic_file_name.txt index 415e0c73..bd99cf95 100644 --- a/src/main/resources/protect_dic_file_name.txt +++ b/src/main/resources/protect_dic_file_name.txt @@ -10,4 +10,4 @@ classes.dex AndroidManifest.xml AndroidManifest -resources.arsc \ No newline at end of file +resources.arsc