├── settings.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── arsceditor ├── src │ └── main │ │ └── java │ │ └── com │ │ ├── mrikso │ │ └── arsceditor │ │ │ ├── intrefaces │ │ │ └── TableChangedListener.java │ │ │ ├── App.java │ │ │ ├── valueeditor │ │ │ ├── ValueType.java │ │ │ ├── ArscWriter.java │ │ │ ├── ValueHelper.java │ │ │ ├── FormatValue.java │ │ │ └── Converter.java │ │ │ ├── gui │ │ │ ├── tree │ │ │ │ ├── JNode.java │ │ │ │ ├── ArscTreeCellRenderer.java │ │ │ │ ├── JTableTreeNode.java │ │ │ │ ├── ArscNode.java │ │ │ │ ├── ResourceEntry.java │ │ │ │ ├── ResourceDirectory.java │ │ │ │ ├── ArscTableView.java │ │ │ │ └── ArscTreeView.java │ │ │ ├── treetable │ │ │ │ ├── TreeTableModel.java │ │ │ │ ├── TreeTableModelAdapter.java │ │ │ │ ├── AbstractCellEditor.java │ │ │ │ ├── JTreeTable.java │ │ │ │ ├── TreeTableCellEditor.java │ │ │ │ ├── AbstractTreeTableModel.java │ │ │ │ └── DynamicTreeTableModel.java │ │ │ ├── dialogs │ │ │ │ ├── ErrorDialog.java │ │ │ │ ├── AboutDialog.java │ │ │ │ ├── PackageEditDialog.java │ │ │ │ └── ResourceEditDialog.java │ │ │ └── MainWindow.java │ │ │ ├── model │ │ │ ├── ResId.java │ │ │ ├── ResourceTypeTableModel.java │ │ │ └── ResourceModel.java │ │ │ └── util │ │ │ ├── VersionUtils.java │ │ │ ├── HexUtil.java │ │ │ ├── AttrNameHelper.java │ │ │ ├── DecodeGenUtils.java │ │ │ └── ResourceHelper2.java │ │ └── google │ │ └── devrel │ │ └── gmscore │ │ └── tools │ │ └── apk │ │ └── arsc │ │ ├── XmlNamespaceEndChunk.java │ │ ├── XmlNamespaceStartChunk.java │ │ ├── SerializableResource.java │ │ ├── XmlChunk.java │ │ ├── UnknownChunk.java │ │ ├── XmlNamespaceChunk.java │ │ ├── ChunkWithChunks.java │ │ ├── XmlEndElementChunk.java │ │ ├── XmlCdataChunk.java │ │ ├── BinaryResourceFile.java │ │ ├── PackageUtils.java │ │ ├── XmlResourceMapChunk.java │ │ ├── ResourceTableChunk.java │ │ ├── XmlNodeChunk.java │ │ ├── TypeSpecChunk.java │ │ ├── BinaryResourceIdentifier.java │ │ ├── LibraryChunk.java │ │ ├── XmlAttribute.java │ │ ├── BinaryResourceValue.java │ │ ├── XmlStartElementChunk.java │ │ └── BinaryResourceString.java └── build.gradle ├── .gitignore ├── README.md ├── gradlew.bat └── gradlew /settings.gradle: -------------------------------------------------------------------------------- 1 | include 'arsceditor' -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MrIkso/ArscEditor/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /arsceditor/src/main/java/com/mrikso/arsceditor/intrefaces/TableChangedListener.java: -------------------------------------------------------------------------------- 1 | package com.mrikso.arsceditor.intrefaces; 2 | 3 | public interface TableChangedListener { 4 | 5 | void tableChanged(); 6 | } 7 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### Gradle template 2 | .gradle 3 | /build/ 4 | /.idea 5 | /out/ 6 | 7 | # Ignore Gradle GUI config 8 | gradle-app.setting 9 | 10 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) 11 | !gradle-wrapper.jar 12 | 13 | # Cache of project 14 | .gradletasknamecache 15 | 16 | # # Work around https://youtrack.jetbrains.com/issue/IDEA-116898 17 | # gradle/wrapper/gradle-wrapper.properties 18 | 19 | # Project exclude paths 20 | /arsceditor/build/ 21 | /arsceditor/build/classes/java/main/ -------------------------------------------------------------------------------- /arsceditor/src/main/java/com/mrikso/arsceditor/App.java: -------------------------------------------------------------------------------- 1 | package com.mrikso.arsceditor; 2 | 3 | import com.mrikso.arsceditor.gui.MainWindow; 4 | 5 | import javax.swing.*; 6 | 7 | public class App { 8 | 9 | public static void main(String[] args) { 10 | try { 11 | // UIManager.setDefaultLookAndFeelDecorated 12 | UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); 13 | SwingUtilities.invokeLater(MainWindow::new); 14 | } catch (Exception e) { 15 | e.printStackTrace(); 16 | System.exit(1); 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /arsceditor/src/main/java/com/mrikso/arsceditor/valueeditor/ValueType.java: -------------------------------------------------------------------------------- 1 | package com.mrikso.arsceditor.valueeditor; 2 | 3 | public enum ValueType { 4 | TYPE_STRING(0), 5 | TYPE_REFERENCE(1), 6 | TYPE_ATTRIBUTE(2), 7 | TYPE_COLOR(3), 8 | TYPE_INT_DEC(4), 9 | TYPE_INT_HEX(5), 10 | TYPE_BOOLEAN(6), 11 | TYPE_FLOAT(7), 12 | TYPE_DIMENSION(8), 13 | TYPE_FRACTION(9), 14 | TYPE_DYNAMIC_REFERENCE(10); 15 | private final int value; 16 | 17 | ValueType(int value) { 18 | this.value = value; 19 | } 20 | 21 | public int getValue() { 22 | return value; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /arsceditor/src/main/java/com/mrikso/arsceditor/gui/tree/JNode.java: -------------------------------------------------------------------------------- 1 | package com.mrikso.arsceditor.gui.tree; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | 5 | import javax.swing.*; 6 | import javax.swing.tree.DefaultMutableTreeNode; 7 | 8 | public abstract class JNode extends DefaultMutableTreeNode { 9 | 10 | public JNode(Object name, boolean allowsChildren) { 11 | super(name, allowsChildren); 12 | } 13 | 14 | public JNode() { 15 | super(); 16 | } 17 | 18 | public JNode(Object name) { 19 | super(name); 20 | } 21 | 22 | public abstract Icon getIcon(); 23 | 24 | @NotNull 25 | public abstract String getName(); 26 | 27 | } 28 | -------------------------------------------------------------------------------- /arsceditor/src/main/java/com/mrikso/arsceditor/valueeditor/ArscWriter.java: -------------------------------------------------------------------------------- 1 | package com.mrikso.arsceditor.valueeditor; 2 | 3 | import com.google.common.io.Files; 4 | import com.google.devrel.gmscore.tools.apk.arsc.ResourceTableChunk; 5 | 6 | import java.io.File; 7 | import java.io.IOException; 8 | 9 | public class ArscWriter { 10 | 11 | private final ResourceTableChunk resourceTableChunk; 12 | private final File out; 13 | 14 | public ArscWriter(ResourceTableChunk chunk, File out){ 15 | this.resourceTableChunk = chunk; 16 | this.out = out; 17 | } 18 | 19 | public void write() throws IOException { 20 | byte[] bytes = resourceTableChunk.toByteArray(); 21 | Files.write(bytes, out); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /arsceditor/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'application' 3 | id 'com.github.johnrengelman.shadow' version '6.1.0' 4 | } 5 | 6 | repositories { 7 | mavenCentral() 8 | } 9 | 10 | dependencies { 11 | implementation 'com.google.guava:guava:30.1.1-jre' 12 | compileOnly 'org.jetbrains:annotations:20.1.0' 13 | 14 | testCompileOnly 'org.jetbrains:annotations:20.1.0' 15 | testCompile group: 'junit', name: 'junit', version: '4.12' 16 | } 17 | 18 | application { 19 | applicationName = 'arsceditor' 20 | mainClassName = 'com.mrikso.arsceditor.App' 21 | } 22 | 23 | jar{ 24 | manifest { 25 | attributes( 26 | "Main-Class": mainClassName, 27 | "Class-Path": configurations.runtimeClasspath.collect { it.getName() }.join(' ') 28 | ) 29 | } 30 | } 31 | 32 | shadowJar { 33 | mergeServiceFiles() 34 | } 35 | -------------------------------------------------------------------------------- /arsceditor/src/main/java/com/google/devrel/gmscore/tools/apk/arsc/XmlNamespaceEndChunk.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.google.devrel.gmscore.tools.apk.arsc; 18 | 19 | import javax.annotation.Nullable; 20 | import java.nio.ByteBuffer; 21 | 22 | /** Represents the ending tag of a namespace in an XML document. */ 23 | public final class XmlNamespaceEndChunk extends XmlNamespaceChunk { 24 | 25 | protected XmlNamespaceEndChunk(ByteBuffer buffer, @Nullable Chunk parent) { 26 | super(buffer, parent); 27 | } 28 | 29 | @Override 30 | protected Type getType() { 31 | return Type.XML_END_NAMESPACE; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /arsceditor/src/main/java/com/google/devrel/gmscore/tools/apk/arsc/XmlNamespaceStartChunk.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.google.devrel.gmscore.tools.apk.arsc; 18 | 19 | import javax.annotation.Nullable; 20 | import java.nio.ByteBuffer; 21 | 22 | /** Represents the starting tag of a namespace in an XML document. */ 23 | public final class XmlNamespaceStartChunk extends XmlNamespaceChunk { 24 | 25 | protected XmlNamespaceStartChunk(ByteBuffer buffer, @Nullable Chunk parent) { 26 | super(buffer, parent); 27 | } 28 | 29 | @Override 30 | protected Type getType() { 31 | return Type.XML_START_NAMESPACE; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /arsceditor/src/main/java/com/mrikso/arsceditor/valueeditor/ValueHelper.java: -------------------------------------------------------------------------------- 1 | package com.mrikso.arsceditor.valueeditor; 2 | 3 | import com.google.devrel.gmscore.tools.apk.arsc.ResourceTableChunk; 4 | import com.google.devrel.gmscore.tools.apk.arsc.StringPoolChunk; 5 | import com.google.devrel.gmscore.tools.apk.arsc.TypeChunk; 6 | 7 | public class ValueHelper { 8 | private static StringPoolChunk stringPoolChunk; 9 | 10 | private static ResourceTableChunk resourceTableChunk; 11 | private static TypeChunk typeChunk; 12 | 13 | public static StringPoolChunk getStringPoolChunk() { 14 | return stringPoolChunk; 15 | } 16 | 17 | public static void setStringPoolChunk(StringPoolChunk stringPoolChunk) { 18 | ValueHelper.stringPoolChunk = stringPoolChunk; 19 | } 20 | 21 | public static ResourceTableChunk getResourceTableChunk() { 22 | return resourceTableChunk; 23 | } 24 | 25 | public static void setResourceTableChunk(ResourceTableChunk resourceTableChunk) { 26 | ValueHelper.resourceTableChunk = resourceTableChunk; 27 | } 28 | 29 | public static TypeChunk getTypeChunk() { 30 | return typeChunk; 31 | } 32 | 33 | public static void setTypeChunk(TypeChunk typeChunk) { 34 | ValueHelper.typeChunk = typeChunk; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Arsc Editor 2 | 3 | 4 | [![GitHub release](https://img.shields.io/github/v/release/MrIkso/ArscEditor)](https://github.com/MrIkso/ArscEditor/releases) [![License](https://img.shields.io/github/license/MrIkso/ArscEditor?color=blue)](LICENSE) 5 | 6 | An open source editor for resources.arsc with GUI. 7 | 8 | This tool used this open source project 9 | - [ArscBlamer](https://github.com/google/android-arscblamer) for parsing and editing resources.arsc files 10 | - [robolectric](https://github.com/robolectric/robolectric) for back convert values to binary format 11 | 12 | ## License 13 | 14 | Copyright (C) 2021 Mr Isko 15 | 16 | This program is free software: you can redistribute it and/or modify 17 | it under the terms of the GNU General Public License as published by 18 | the Free Software Foundation, either version 3 of the License, or 19 | (at your option) any later version. 20 | 21 | This program is distributed in the hope that it will be useful, 22 | but WITHOUT ANY WARRANTY; without even the implied warranty of 23 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 24 | GNU General Public License for more details. 25 | 26 | You should have received a copy of the GNU General Public License 27 | along with this program. If not, see . 28 | -------------------------------------------------------------------------------- /arsceditor/src/main/java/com/mrikso/arsceditor/model/ResId.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021 MrIkso 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU General Public License 15 | * along with this program. If not, see . 16 | */ 17 | 18 | package com.mrikso.arsceditor.model; 19 | 20 | public class ResId { 21 | private final int id; 22 | private final String name; 23 | private final String type; 24 | 25 | public ResId(int id, String name, String type){ 26 | this.id = id; 27 | this.name = name; 28 | this.type = type; 29 | } 30 | 31 | public int getId() { 32 | return id; 33 | } 34 | 35 | public String getName() { 36 | return name; 37 | } 38 | 39 | public String getType() { 40 | return type; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /arsceditor/src/main/java/com/mrikso/arsceditor/valueeditor/FormatValue.java: -------------------------------------------------------------------------------- 1 | package com.mrikso.arsceditor.valueeditor; 2 | 3 | public class FormatValue { 4 | 5 | private final ValueType valueType; 6 | private final String value; 7 | private String decodedNamedValue; 8 | private int index; 9 | 10 | public FormatValue(ValueType valueType, String value, int index){ 11 | this.valueType = valueType; 12 | this.value = value; 13 | this.index = index; 14 | } 15 | 16 | public FormatValue(ValueType valueType, String value){ 17 | this.valueType = valueType; 18 | this.value = value; 19 | } 20 | 21 | public FormatValue(ValueType valueType, String value, int index, String decodedNamedValue){ 22 | this.valueType = valueType; 23 | this.value = value; 24 | this.index = index; 25 | this.decodedNamedValue = decodedNamedValue; 26 | } 27 | 28 | public FormatValue(ValueType valueType, String value, String decodedNamedValue){ 29 | this.valueType = valueType; 30 | this.value = value; 31 | this.decodedNamedValue = decodedNamedValue; 32 | } 33 | 34 | public int getIndex() { 35 | return index; 36 | } 37 | 38 | public String getValue() { 39 | return value; 40 | } 41 | 42 | public String getDecodedNamedValue() { 43 | return decodedNamedValue; 44 | } 45 | 46 | public ValueType getValueType() { 47 | return valueType; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /arsceditor/src/main/java/com/google/devrel/gmscore/tools/apk/arsc/SerializableResource.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.google.devrel.gmscore.tools.apk.arsc; 18 | 19 | import java.io.IOException; 20 | 21 | /** 22 | * A resource, typically a @{link Chunk}, that can be converted to an array of bytes. 23 | */ 24 | public interface SerializableResource { 25 | 26 | /** 27 | * Converts this resource into an array of bytes representation. 28 | * @return An array of bytes representing this resource. 29 | * @throws IOException 30 | */ 31 | byte[] toByteArray() throws IOException; 32 | 33 | /** 34 | * Converts this resource into an array of bytes representation. 35 | * @param shrink True if, when converting to a byte array, this resource can modify the returned 36 | * bytes in an effort to reduce the size. 37 | * @return An array of bytes representing this resource. 38 | * @throws IOException 39 | */ 40 | byte[] toByteArray(boolean shrink) throws IOException; 41 | } 42 | -------------------------------------------------------------------------------- /arsceditor/src/main/java/com/mrikso/arsceditor/gui/treetable/TreeTableModel.java: -------------------------------------------------------------------------------- 1 | package com.mrikso.arsceditor.gui.treetable; 2 | 3 | import javax.swing.event.TableModelListener; 4 | import javax.swing.tree.TreeModel; 5 | 6 | public interface TreeTableModel extends TreeModel { 7 | /** 8 | * Returns the number of available columns. 9 | */ 10 | public int getColumnCount(); 11 | 12 | /** 13 | * Returns the name for column number column. 14 | */ 15 | public String getColumnName(int column); 16 | 17 | /** 18 | * Returns the type for column number column. 19 | */ 20 | public Class getColumnClass(int column); 21 | 22 | /** 23 | * Returns the value to be displayed for node node, at column 24 | * number column. 25 | */ 26 | public Object getValueAt(Object node, int column); 27 | 28 | /** 29 | * Indicates whether the the value for node node, at column 30 | * number column is editable. 31 | */ 32 | public boolean isCellEditable(Object node, int column); 33 | 34 | /** 35 | * Sets the value for node node, at column number 36 | * column. 37 | */ 38 | public void setValueAt(Object aValue, Object node, int column); 39 | 40 | /** 41 | * Adds a listener to the list that is notified each time a change to the 42 | * data model occurs. 43 | */ 44 | public void addTableModelListener(TableModelListener l); 45 | 46 | /** 47 | * Removes a listener from the list that is notified each time a change to 48 | * the data model occurs. 49 | */ 50 | public void removeTableModelListener(TableModelListener l); 51 | } 52 | -------------------------------------------------------------------------------- /arsceditor/src/main/java/com/google/devrel/gmscore/tools/apk/arsc/XmlChunk.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.google.devrel.gmscore.tools.apk.arsc; 18 | 19 | import javax.annotation.Nullable; 20 | import java.nio.ByteBuffer; 21 | 22 | /** 23 | * Represents an XML chunk structure. 24 | * 25 | *

An XML chunk can contain many nodes as well as a string pool which contains all of the strings 26 | * referenced by the nodes. 27 | */ 28 | public final class XmlChunk extends ChunkWithChunks { 29 | 30 | protected XmlChunk(ByteBuffer buffer, @Nullable Chunk parent) { 31 | super(buffer, parent); 32 | } 33 | 34 | @Override 35 | protected Type getType() { 36 | return Type.XML; 37 | } 38 | 39 | /** Returns a string at the provided (0-based) index if the index exists in the string pool. */ 40 | public String getString(int index) { 41 | for (Chunk chunk : getChunks().values()) { 42 | if (chunk instanceof StringPoolChunk) { 43 | return ((StringPoolChunk) chunk).getString(index); 44 | } 45 | } 46 | throw new IllegalStateException("XmlChunk did not contain a string pool."); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /arsceditor/src/main/java/com/mrikso/arsceditor/util/VersionUtils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021 MrIkso 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU General Public License 15 | * along with this program. If not, see . 16 | */ 17 | 18 | package com.mrikso.arsceditor.util; 19 | 20 | import java.io.InputStream; 21 | import java.net.URL; 22 | import java.util.Enumeration; 23 | import java.util.jar.Manifest; 24 | 25 | public class VersionUtils { 26 | public static String getVersion() { 27 | try { 28 | ClassLoader classLoader = VersionUtils.class.getClassLoader(); 29 | if (classLoader != null) { 30 | Enumeration resources = classLoader.getResources("META-INF/MANIFEST.MF"); 31 | while (resources.hasMoreElements()) { 32 | try (InputStream is = resources.nextElement().openStream()) { 33 | Manifest manifest = new Manifest(is); 34 | String ver = manifest.getMainAttributes().getValue("arsceditor-version"); 35 | if (ver != null) { 36 | return ver; 37 | } 38 | } 39 | } 40 | } 41 | } catch (Exception e) { 42 | e.printStackTrace(); 43 | } 44 | return "null"; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /arsceditor/src/main/java/com/mrikso/arsceditor/gui/tree/ArscTreeCellRenderer.java: -------------------------------------------------------------------------------- 1 | package com.mrikso.arsceditor.gui.tree; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | 5 | import javax.swing.*; 6 | import javax.swing.tree.DefaultTreeCellRenderer; 7 | import java.awt.*; 8 | 9 | public class ArscTreeCellRenderer extends DefaultTreeCellRenderer { 10 | 11 | @Override 12 | public Component getTreeCellRendererComponent(@NotNull JTree tree, 13 | Object value, 14 | boolean selected, 15 | boolean expanded, 16 | boolean leaf, 17 | int row, 18 | boolean hasFocus) { 19 | super.getTreeCellRendererComponent(tree, value, selected, expanded, leaf, row, hasFocus); 20 | if (value instanceof ArscNode) { 21 | ArscNode node = (ArscNode) value; 22 | setText(node.getName()); 23 | setIcon(node.getIcon()); 24 | } 25 | else { 26 | if(value!= null) { 27 | setText(value.toString()); 28 | }else { 29 | setText(null); 30 | } 31 | setIcon(new TreeIcon()); 32 | } 33 | return this; 34 | } 35 | 36 | 37 | static class TreeIcon implements Icon { 38 | 39 | private int SIZE = 0; 40 | 41 | public TreeIcon() { 42 | } 43 | 44 | public int getIconWidth() { 45 | return SIZE; 46 | } 47 | 48 | public int getIconHeight() { 49 | return SIZE; 50 | } 51 | 52 | public void paintIcon(Component c, Graphics g, int x, int y) { 53 | //System.out.println(c.getWidth() + " " + c.getHeight() + " " + x + " " + y); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /arsceditor/src/main/java/com/mrikso/arsceditor/gui/tree/JTableTreeNode.java: -------------------------------------------------------------------------------- 1 | package com.mrikso.arsceditor.gui.tree; 2 | 3 | import com.mrikso.arsceditor.valueeditor.ValueType; 4 | 5 | import javax.swing.tree.DefaultMutableTreeNode; 6 | 7 | public abstract class JTableTreeNode extends DefaultMutableTreeNode { 8 | 9 | public JTableTreeNode() { 10 | super(); 11 | } 12 | 13 | public JTableTreeNode(String id) { 14 | super(id); 15 | } 16 | 17 | public abstract String getName(); 18 | 19 | public abstract void setName(String name); 20 | 21 | public abstract String getDecodedName(); 22 | 23 | public abstract void setDecodedName(String name); 24 | 25 | public abstract void setId(String id); 26 | 27 | public abstract String getId(); 28 | 29 | public abstract void setNameIndex(int index); 30 | 31 | public abstract int getNameIndex(); 32 | 33 | public abstract void setValueIndex(int index); 34 | 35 | public abstract int getValueIndex(); 36 | 37 | public abstract void setEntryIndex(int index); 38 | 39 | public abstract int getEntryIndex(); 40 | 41 | public abstract void setEntryChildrenIndex(int index); 42 | 43 | public abstract int getEntryChildrenIndex(); 44 | 45 | public abstract String getValue(); 46 | 47 | public abstract void setValue(String value); 48 | 49 | public abstract String getDecodeValue(); 50 | 51 | public abstract void setDecodedValue(String value); 52 | 53 | public abstract ValueType getValueType(); 54 | 55 | public abstract void setValueType(ValueType value); 56 | 57 | // public abstract TypeChunk getTypeChunk(); 58 | 59 | // public abstract void setTypeChunk(TypeChunk value); 60 | 61 | public abstract boolean isComplex(); 62 | 63 | public abstract void setIsComplex(boolean isComplex); 64 | 65 | public abstract boolean isChildren(); 66 | 67 | public abstract void setIsChildren(boolean isChildren); 68 | } 69 | -------------------------------------------------------------------------------- /arsceditor/src/main/java/com/mrikso/arsceditor/gui/dialogs/ErrorDialog.java: -------------------------------------------------------------------------------- 1 | package com.mrikso.arsceditor.gui.dialogs; 2 | 3 | import com.google.common.base.Throwables; 4 | 5 | import javax.swing.*; 6 | import java.awt.*; 7 | import java.awt.datatransfer.StringSelection; 8 | 9 | public class ErrorDialog extends JDialog { 10 | 11 | public ErrorDialog(JFrame owner, Exception error){ 12 | super(owner); 13 | 14 | JPanel pContent = new JPanel(new BorderLayout()); 15 | JTextArea textArea = new JTextArea(30, 70); 16 | textArea.setEditable(false); 17 | textArea.setText(Throwables.getStackTraceAsString(error)); 18 | textArea.setCaretPosition(0); 19 | textArea.setFont(textArea.getFont().deriveFont(12f)); 20 | JScrollPane scrollPane = new JScrollPane(textArea); 21 | JButton copyError = new JButton("Copy Error"); 22 | copyError.addActionListener(l-> copyTextToClipboard(Throwables.getStackTraceAsString(error))); 23 | JButton okButton = new JButton("OK"); 24 | okButton.addActionListener(l-> setVisible(false)); 25 | JPanel pButtons = new JPanel(new FlowLayout(FlowLayout.CENTER)); 26 | pButtons.add(copyError); 27 | pButtons.add(okButton); 28 | 29 | pContent.add(scrollPane, BorderLayout.CENTER); 30 | pContent.add(pButtons, BorderLayout.SOUTH); 31 | 32 | getContentPane().add(pContent); 33 | 34 | pack(); 35 | //setSize(new Dimension(100, 300)); 36 | setModal(true); 37 | setResizable(false); 38 | setTitle("Error"); 39 | setLocationRelativeTo(owner); 40 | setVisible(true); 41 | } 42 | 43 | private void copyTextToClipboard(String text){ 44 | Toolkit.getDefaultToolkit() 45 | .getSystemClipboard() 46 | .setContents( 47 | new StringSelection(text), 48 | null 49 | ); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /arsceditor/src/main/java/com/mrikso/arsceditor/model/ResourceTypeTableModel.java: -------------------------------------------------------------------------------- 1 | package com.mrikso.arsceditor.model; 2 | 3 | import com.mrikso.arsceditor.gui.tree.JTableTreeNode; 4 | import com.mrikso.arsceditor.gui.treetable.DynamicTreeTableModel; 5 | import com.mrikso.arsceditor.gui.treetable.TreeTableModel; 6 | 7 | public class ResourceTypeTableModel extends DynamicTreeTableModel { 8 | /** 9 | * Names of the columns. 10 | */ 11 | private static final String[] columnNames = { "ID", "Name", 12 | "Value"}; 13 | /** 14 | * Method names used to access the data to display. 15 | */ 16 | private static final String[] methodNames = { "getId", "getName", 17 | "getValue" }; 18 | /** 19 | * Method names used to set the data. 20 | */ 21 | private static final String[] setterMethodNames = { "setId","setName", 22 | "setValue"}; 23 | /** 24 | * Classes presenting the data. 25 | */ 26 | private static final Class[] classes = { TreeTableModel.class, 27 | String.class, String.class, String.class}; 28 | /** 29 | * Constructor for creating a DynamicTreeTableModel. 30 | * 31 | * @param root 32 | */ 33 | public ResourceTypeTableModel(JTableTreeNode root) { 34 | super(root, columnNames, methodNames, setterMethodNames, classes); 35 | } 36 | 37 | 38 | @Override 39 | public boolean isCellEditable(Object node, int column) { 40 | switch (column) { 41 | case 0: 42 | // Allow editing of the name, as long as not the root. 43 | return (node != getRoot()); 44 | case 1: 45 | // Allow editing of the location, as long as not a 46 | // directory 47 | return (node instanceof ResourceModel); 48 | default: 49 | // Don't allow editing of the date fields. 50 | return false; 51 | } 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /arsceditor/src/main/java/com/google/devrel/gmscore/tools/apk/arsc/UnknownChunk.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.google.devrel.gmscore.tools.apk.arsc; 18 | 19 | import javax.annotation.Nullable; 20 | import java.io.DataOutput; 21 | import java.io.IOException; 22 | import java.nio.ByteBuffer; 23 | 24 | /** 25 | * A chunk whose contents are unknown. This is a placeholder until we add a proper chunk for the 26 | * unknown type. 27 | */ 28 | public final class UnknownChunk extends Chunk { 29 | 30 | private final Type type; 31 | 32 | private final byte[] header; 33 | 34 | private final byte[] payload; 35 | 36 | protected UnknownChunk(ByteBuffer buffer, @Nullable Chunk parent) { 37 | super(buffer, parent); 38 | 39 | type = Type.fromCode(buffer.getShort(offset)); 40 | header = new byte[headerSize - Chunk.METADATA_SIZE]; 41 | payload = new byte[chunkSize - headerSize]; 42 | buffer.get(header); 43 | buffer.get(payload); 44 | } 45 | 46 | @Override 47 | protected void writeHeader(ByteBuffer output) { 48 | output.put(header); 49 | } 50 | 51 | @Override 52 | protected void writePayload(DataOutput output, ByteBuffer header, boolean shrink) 53 | throws IOException { 54 | output.write(payload); 55 | } 56 | 57 | @Override 58 | protected Type getType() { 59 | return type; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /arsceditor/src/main/java/com/mrikso/arsceditor/gui/treetable/TreeTableModelAdapter.java: -------------------------------------------------------------------------------- 1 | package com.mrikso.arsceditor.gui.treetable; 2 | 3 | import javax.swing.JTree; 4 | import javax.swing.event.TreeExpansionEvent; 5 | import javax.swing.event.TreeExpansionListener; 6 | import javax.swing.table.AbstractTableModel; 7 | import javax.swing.tree.TreePath; 8 | 9 | public class TreeTableModelAdapter extends AbstractTableModel { 10 | /** 11 | * 12 | */ 13 | private static final long serialVersionUID = 1L; 14 | private final JTree tree; 15 | private final TreeTableModel treeTableModel; 16 | 17 | public TreeTableModelAdapter(TreeTableModel treeTableModel, JTree tree) { 18 | this.tree = tree; 19 | this.treeTableModel = treeTableModel; 20 | 21 | tree.addTreeExpansionListener(new TreeExpansionListener() { 22 | // Don't use fireTableRowsInserted() here; 23 | // the selection model would get updated twice. 24 | public void treeExpanded(TreeExpansionEvent event) { 25 | fireTableDataChanged(); 26 | } 27 | 28 | public void treeCollapsed(TreeExpansionEvent event) { 29 | fireTableDataChanged(); 30 | } 31 | }); 32 | } 33 | 34 | // Wrappers, implementing TableModel interface. 35 | 36 | public int getColumnCount() { 37 | return treeTableModel.getColumnCount(); 38 | } 39 | 40 | public String getColumnName(int column) { 41 | return treeTableModel.getColumnName(column); 42 | } 43 | 44 | public Class getColumnClass(int column) { 45 | return treeTableModel.getColumnClass(column); 46 | } 47 | 48 | public int getRowCount() { 49 | return tree.getRowCount(); 50 | } 51 | 52 | protected Object nodeForRow(int row) { 53 | TreePath treePath = tree.getPathForRow(row); 54 | if(treePath !=null){ 55 | return treePath.getLastPathComponent();} 56 | return null; 57 | } 58 | 59 | @Override 60 | public Object getValueAt(int row, int column) { 61 | return treeTableModel.getValueAt(nodeForRow(row), column); 62 | } 63 | 64 | @Override 65 | public boolean isCellEditable(int row, int column) { 66 | return treeTableModel.isCellEditable(nodeForRow(row), column); 67 | } 68 | @Override 69 | public void setValueAt(Object value, int row, int column) { 70 | treeTableModel.setValueAt(value, nodeForRow(row), column); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /arsceditor/src/main/java/com/mrikso/arsceditor/gui/treetable/AbstractCellEditor.java: -------------------------------------------------------------------------------- 1 | package com.mrikso.arsceditor.gui.treetable; 2 | 3 | import java.util.EventObject; 4 | import javax.swing.CellEditor; 5 | import javax.swing.event.CellEditorListener; 6 | import javax.swing.event.ChangeEvent; 7 | import javax.swing.event.EventListenerList; 8 | 9 | public class AbstractCellEditor implements CellEditor { 10 | 11 | protected EventListenerList listenerList = new EventListenerList(); 12 | 13 | public Object getCellEditorValue() { 14 | return null; 15 | } 16 | 17 | public boolean isCellEditable(EventObject e) { 18 | return true; 19 | } 20 | 21 | public boolean shouldSelectCell(EventObject anEvent) { 22 | return false; 23 | } 24 | 25 | public boolean stopCellEditing() { 26 | return true; 27 | } 28 | 29 | public void cancelCellEditing() { 30 | } 31 | 32 | public void addCellEditorListener(CellEditorListener l) { 33 | listenerList.add(CellEditorListener.class, l); 34 | } 35 | 36 | public void removeCellEditorListener(CellEditorListener l) { 37 | listenerList.remove(CellEditorListener.class, l); 38 | } 39 | 40 | /** 41 | * Notify all listeners that have registered interest for notification on 42 | * this event type. 43 | * 44 | * @see EventListenerList 45 | */ 46 | protected void fireEditingStopped() { 47 | // Guaranteed to return a non-null array 48 | Object[] listeners = listenerList.getListenerList(); 49 | // Process the listeners last to first, notifying 50 | // those that are interested in this event 51 | for (int i = listeners.length - 2; i >= 0; i -= 2) { 52 | if (listeners[i] == CellEditorListener.class) { 53 | ((CellEditorListener) listeners[i + 1]) 54 | .editingStopped(new ChangeEvent(this)); 55 | } 56 | } 57 | } 58 | 59 | /** 60 | * Notify all listeners that have registered interest for notification on 61 | * this event type. 62 | * 63 | * @see EventListenerList 64 | */ 65 | protected void fireEditingCanceled() { 66 | // Guaranteed to return a non-null array 67 | Object[] listeners = listenerList.getListenerList(); 68 | // Process the listeners last to first, notifying 69 | // those that are interested in this event 70 | for (int i = listeners.length - 2; i >= 0; i -= 2) { 71 | if (listeners[i] == CellEditorListener.class) { 72 | ((CellEditorListener) listeners[i + 1]) 73 | .editingCanceled(new ChangeEvent(this)); 74 | } 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /arsceditor/src/main/java/com/mrikso/arsceditor/gui/dialogs/AboutDialog.java: -------------------------------------------------------------------------------- 1 | package com.mrikso.arsceditor.gui.dialogs; 2 | 3 | import com.mrikso.arsceditor.util.VersionUtils; 4 | 5 | import javax.swing.*; 6 | import java.awt.*; 7 | 8 | public class AboutDialog extends JDialog { 9 | /** 10 | * @param owner 11 | */ 12 | public AboutDialog(final JFrame owner) { 13 | 14 | setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE); 15 | setModal(true); 16 | 17 | initComponents(); 18 | pack(); 19 | setTitle("About"); 20 | setLocationRelativeTo(owner); 21 | setResizable(false); 22 | } 23 | 24 | private void initComponents() { 25 | setLayout(new FlowLayout()); 26 | 27 | final JButton buttonClose = new JButton("Close"); 28 | buttonClose.addActionListener(e -> setVisible(false)); 29 | 30 | final JPanel listPane = new JPanel(); 31 | listPane.setLayout(new BoxLayout(listPane, BoxLayout.PAGE_AXIS)); 32 | 33 | final JLabel label = new JLabel("Arsc Editor"); 34 | label.setFont(new Font(Font.DIALOG, Font.BOLD, 20)); 35 | listPane.add(label); 36 | listPane.add(Box.createRigidArea(new Dimension(0, 5))); 37 | listPane.add(new JLabel(String.format("Version: %s", VersionUtils.getVersion()))); 38 | listPane.add(Box.createRigidArea(new Dimension(0, 5))); 39 | listPane.add(new JLabel("Author: Mr Ikso")); 40 | listPane.add(Box.createRigidArea(new Dimension(0, 20))); 41 | listPane.add(Box.createRigidArea(new Dimension(0, 5))); 42 | listPane.add(new JLabel("Free open source tool to editing *.arsc files.")); 43 | listPane.add(Box.createRigidArea(new Dimension(0, 5))); 44 | 45 | listPane.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); 46 | 47 | final JPanel buttonPane = new JPanel(); 48 | buttonPane.setLayout(new FlowLayout(FlowLayout.CENTER)); 49 | buttonPane.setBorder(BorderFactory.createEmptyBorder(0, 10, 10, 10)); 50 | buttonPane.add(buttonClose); 51 | 52 | final JPanel root = new JPanel(); 53 | root.setLayout(new BorderLayout()); 54 | root.add(Box.createVerticalGlue()); 55 | root.add(listPane, BorderLayout.CENTER); 56 | root.add(buttonPane, BorderLayout.SOUTH); 57 | add(root); 58 | 59 | } 60 | 61 | public void showDialog() { 62 | setVisible(true); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /arsceditor/src/main/java/com/google/devrel/gmscore/tools/apk/arsc/XmlNamespaceChunk.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.google.devrel.gmscore.tools.apk.arsc; 18 | 19 | import javax.annotation.Nullable; 20 | import java.io.DataOutput; 21 | import java.io.IOException; 22 | import java.nio.ByteBuffer; 23 | 24 | /** Represents the start/end of a namespace in an XML document. */ 25 | public abstract class XmlNamespaceChunk extends XmlNodeChunk { 26 | 27 | /** A string reference to the namespace prefix. */ 28 | private final int prefix; 29 | 30 | /** A string reference to the namespace URI. */ 31 | private final int uri; 32 | 33 | protected XmlNamespaceChunk(ByteBuffer buffer, @Nullable Chunk parent) { 34 | super(buffer, parent); 35 | prefix = buffer.getInt(); 36 | uri = buffer.getInt(); 37 | } 38 | 39 | /** Returns the namespace prefix. */ 40 | public String getPrefix() { 41 | return getString(prefix); 42 | } 43 | 44 | /** Returns the namespace URI. */ 45 | public String getUri() { 46 | return getString(uri); 47 | } 48 | 49 | @Override 50 | protected void writePayload(DataOutput output, ByteBuffer header, boolean shrink) 51 | throws IOException { 52 | super.writePayload(output, header, shrink); 53 | output.writeInt(prefix); 54 | output.writeInt(uri); 55 | } 56 | 57 | /** 58 | * Returns a brief description of this namespace chunk. The representation of this information is 59 | * subject to change, but below is a typical example: 60 | * 61 | *

62 |    * "XmlNamespaceChunk{line=1234, comment=My awesome comment., prefix=foo, uri=com.google.foo}"
63 |    * 
64 | */ 65 | @Override 66 | public String toString() { 67 | return String.format("XmlNamespaceChunk{line=%d, comment=%s, prefix=%s, uri=%s}", 68 | getLineNumber(), getComment(), getPrefix(), getUri()); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /arsceditor/src/main/java/com/google/devrel/gmscore/tools/apk/arsc/ChunkWithChunks.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.google.devrel.gmscore.tools.apk.arsc; 18 | 19 | import javax.annotation.Nullable; 20 | import java.io.DataOutput; 21 | import java.io.IOException; 22 | import java.nio.ByteBuffer; 23 | import java.util.LinkedHashMap; 24 | import java.util.Map; 25 | 26 | /** Represents a chunk whose payload is a list of sub-chunks. */ 27 | public abstract class ChunkWithChunks extends Chunk { 28 | 29 | private final Map chunks = new LinkedHashMap<>(); 30 | 31 | protected ChunkWithChunks(ByteBuffer buffer, @Nullable Chunk parent) { 32 | super(buffer, parent); 33 | } 34 | 35 | @Override 36 | protected void init(ByteBuffer buffer) { 37 | super.init(buffer); 38 | chunks.clear(); 39 | int start = this.offset + getHeaderSize(); 40 | int offset = start; 41 | int end = this.offset + getOriginalChunkSize(); 42 | int position = buffer.position(); 43 | buffer.position(start); 44 | 45 | while (offset < end) { 46 | Chunk chunk = Chunk.newInstance(buffer, this); 47 | chunks.put(offset, chunk); 48 | offset += chunk.getOriginalChunkSize(); 49 | } 50 | 51 | buffer.position(position); 52 | } 53 | 54 | /** 55 | * Retrieves the @{code chunks} contained in this chunk. 56 | * 57 | * @return map of buffer offset -> chunk contained in this chunk. 58 | */ 59 | public final Map getChunks() { 60 | return chunks; 61 | } 62 | 63 | @Override 64 | protected void writePayload(DataOutput output, ByteBuffer header, boolean shrink) 65 | throws IOException { 66 | for (Chunk chunk : getChunks().values()) { 67 | byte[] chunkBytes = chunk.toByteArray(shrink); 68 | output.write(chunkBytes); 69 | writePad(output, chunkBytes.length); 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /arsceditor/src/main/java/com/google/devrel/gmscore/tools/apk/arsc/XmlEndElementChunk.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.google.devrel.gmscore.tools.apk.arsc; 18 | 19 | import javax.annotation.Nullable; 20 | import java.io.DataOutput; 21 | import java.io.IOException; 22 | import java.nio.ByteBuffer; 23 | 24 | /** Represents the end of an XML node. */ 25 | public final class XmlEndElementChunk extends XmlNodeChunk { 26 | 27 | /** A string reference to the namespace URI, or -1 if not present. */ 28 | private final int namespace; 29 | 30 | /** A string reference to the attribute name. */ 31 | private final int name; 32 | 33 | protected XmlEndElementChunk(ByteBuffer buffer, @Nullable Chunk parent) { 34 | super(buffer, parent); 35 | namespace = buffer.getInt(); 36 | name = buffer.getInt(); 37 | } 38 | 39 | /** Returns the namespace URI, or the empty string if no namespace is present. */ 40 | public String getNamespace() { 41 | return getString(namespace); 42 | } 43 | 44 | /** Returns the attribute name. */ 45 | public String getName() { 46 | return getString(name); 47 | } 48 | 49 | @Override 50 | protected Type getType() { 51 | return Type.XML_END_ELEMENT; 52 | } 53 | 54 | @Override 55 | protected void writePayload(DataOutput output, ByteBuffer header, boolean shrink) 56 | throws IOException { 57 | super.writePayload(output, header, shrink); 58 | output.writeInt(namespace); 59 | output.writeInt(name); 60 | } 61 | 62 | /** 63 | * Returns a brief description of this XML node. The representation of this information is 64 | * subject to change, but below is a typical example: 65 | * 66 | *
67 |    * "XmlEndElementChunk{line=1234, comment=My awesome comment., namespace=foo, name=bar}"
68 |    * 
69 | */ 70 | @Override 71 | public String toString() { 72 | return String.format("XmlEndElementChunk{line=%d, comment=%s, namespace=%s, name=%s}", 73 | getLineNumber(), getComment(), getNamespace(), getName()); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /arsceditor/src/main/java/com/mrikso/arsceditor/gui/dialogs/PackageEditDialog.java: -------------------------------------------------------------------------------- 1 | package com.mrikso.arsceditor.gui.dialogs; 2 | 3 | import com.mrikso.arsceditor.gui.tree.ArscNode; 4 | 5 | import javax.swing.*; 6 | import java.awt.*; 7 | 8 | public class PackageEditDialog extends JDialog { 9 | 10 | private ValueChangedListener valueChangedListener; 11 | 12 | public PackageEditDialog(JFrame parent, ArscNode node) { 13 | super(parent); 14 | 15 | JTextField packageNameFiled = new JTextField(20); 16 | packageNameFiled.setText(node.getPackageName()); 17 | 18 | JTextField packageIdField = new JTextField(20); 19 | packageIdField.setText(String.valueOf(node.getId())); 20 | packageIdField.setEditable(false); 21 | 22 | // CompactGrid layout is from http://sourceforge.net/projects/swinglib/ 23 | JPanel pFields = new JPanel(new CompactGridLayout(2, 6, 12)); 24 | pFields.add(new JLabel("Package Name:")); 25 | pFields.add(packageNameFiled); 26 | pFields.add(new JLabel("Package Id:")); 27 | pFields.add(packageIdField); 28 | pFields.setBorder(BorderFactory.createEmptyBorder(12, 12, 12, 12)); 29 | 30 | JButton okButton = new JButton("OK"); 31 | okButton.addActionListener(l -> { 32 | save(packageNameFiled.getText(), Integer.parseInt(packageIdField.getText())); 33 | }); 34 | JButton cancelButton = new JButton("Cancel"); 35 | cancelButton.addActionListener(l -> { 36 | setVisible(false); 37 | }); 38 | JPanel pButtons = new JPanel(new FlowLayout(FlowLayout.CENTER)); 39 | pButtons.add(okButton); 40 | pButtons.add(cancelButton); 41 | 42 | JPanel pContent = new JPanel(new BorderLayout()); 43 | pContent.add(pFields, BorderLayout.CENTER); 44 | pContent.add(pButtons, BorderLayout.SOUTH); 45 | 46 | setContentPane(pContent); 47 | pack(); 48 | // setSize(new Dimension(300, 100)); 49 | setTitle("Edit Package"); 50 | setLocationRelativeTo(parent); 51 | setModal(true); 52 | setResizable(false); 53 | } 54 | 55 | 56 | private void save(String name, int id) { 57 | if (valueChangedListener != null) { 58 | valueChangedListener.onEdited(name, id); 59 | } 60 | setVisible(false); 61 | } 62 | 63 | public void setValueChangedListener(ValueChangedListener valueChangedListener) { 64 | this.valueChangedListener = valueChangedListener; 65 | } 66 | 67 | public void showDialog() { 68 | setVisible(true); 69 | } 70 | 71 | public interface ValueChangedListener { 72 | public void onEdited(String name, int id); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /arsceditor/src/main/java/com/google/devrel/gmscore/tools/apk/arsc/XmlCdataChunk.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.google.devrel.gmscore.tools.apk.arsc; 18 | 19 | import javax.annotation.Nullable; 20 | import java.io.DataOutput; 21 | import java.io.IOException; 22 | import java.nio.ByteBuffer; 23 | 24 | /** Represents an XML cdata node. */ 25 | public final class XmlCdataChunk extends XmlNodeChunk { 26 | 27 | /** A string reference to a string containing the raw character data. */ 28 | private final int rawValue; 29 | 30 | /** A {@link BinaryResourceValue} instance containing the parsed value. */ 31 | private final BinaryResourceValue binaryResourceValue; 32 | 33 | protected XmlCdataChunk(ByteBuffer buffer, @Nullable Chunk parent) { 34 | super(buffer, parent); 35 | rawValue = buffer.getInt(); 36 | binaryResourceValue = BinaryResourceValue.create(buffer); 37 | } 38 | 39 | /** Returns a string containing the raw character data of this chunk. */ 40 | public String getRawValue() { 41 | return getString(rawValue); 42 | } 43 | 44 | /** Returns a {@link BinaryResourceValue} instance containing the parsed cdata value. */ 45 | public BinaryResourceValue getResourceValue() { 46 | return binaryResourceValue; 47 | } 48 | 49 | @Override 50 | protected Type getType() { 51 | return Type.XML_CDATA; 52 | } 53 | 54 | @Override 55 | protected void writePayload(DataOutput output, ByteBuffer header, boolean shrink) 56 | throws IOException { 57 | super.writePayload(output, header, shrink); 58 | output.writeInt(rawValue); 59 | output.write(binaryResourceValue.toByteArray()); 60 | } 61 | 62 | /** 63 | * Returns a brief description of this XML node. The representation of this information is 64 | * subject to change, but below is a typical example: 65 | * 66 | *
"XmlCdataChunk{line=1234, comment=My awesome comment., value=1234}"
67 | */ 68 | @Override 69 | public String toString() { 70 | return String.format("XmlCdataChunk{line=%d, comment=%s, value=%s}", 71 | getLineNumber(), getComment(), getRawValue()); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /arsceditor/src/main/java/com/google/devrel/gmscore/tools/apk/arsc/BinaryResourceFile.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.google.devrel.gmscore.tools.apk.arsc; 18 | 19 | import com.google.common.io.ByteArrayDataOutput; 20 | import com.google.common.io.ByteStreams; 21 | 22 | import java.io.IOException; 23 | import java.io.InputStream; 24 | import java.nio.ByteBuffer; 25 | import java.nio.ByteOrder; 26 | import java.util.ArrayList; 27 | import java.util.Collections; 28 | import java.util.List; 29 | 30 | /** Given an arsc file, maps the contents of the file. */ 31 | public final class BinaryResourceFile implements SerializableResource { 32 | 33 | /** The chunks contained in this resource file. */ 34 | private final List chunks = new ArrayList<>(); 35 | 36 | public BinaryResourceFile(byte[] buf) { 37 | ByteBuffer buffer = ByteBuffer.wrap(buf).order(ByteOrder.LITTLE_ENDIAN); 38 | while (buffer.remaining() > 0) { 39 | chunks.add(Chunk.newInstance(buffer)); 40 | } 41 | } 42 | 43 | /** 44 | * Given an input stream, reads the stream until the end and returns a {@link BinaryResourceFile} 45 | * representing the contents of the stream. 46 | * 47 | * @param is The input stream to read from. 48 | * @return BinaryResourceFile represented by the @{link InputStream}. 49 | * @throws IOException 50 | */ 51 | public static BinaryResourceFile fromInputStream(InputStream is) throws IOException { 52 | byte[] buf = ByteStreams.toByteArray(is); 53 | return new BinaryResourceFile(buf); 54 | } 55 | 56 | /** Returns the chunks in this resource file. */ 57 | public List getChunks() { 58 | return Collections.unmodifiableList(chunks); 59 | } 60 | 61 | @Override 62 | public byte[] toByteArray() throws IOException { 63 | return toByteArray(false); 64 | } 65 | 66 | @Override 67 | public byte[] toByteArray(boolean shrink) throws IOException { 68 | ByteArrayDataOutput output = ByteStreams.newDataOutput(); 69 | for (Chunk chunk : chunks) { 70 | output.write(chunk.toByteArray(shrink)); 71 | } 72 | return output.toByteArray(); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /arsceditor/src/main/java/com/mrikso/arsceditor/util/HexUtil.java: -------------------------------------------------------------------------------- 1 | package com.mrikso.arsceditor.util; 2 | 3 | import java.io.IOException; 4 | import java.nio.charset.StandardCharsets; 5 | 6 | public class HexUtil { 7 | 8 | public static String byteToHexString(byte a) { 9 | int a1 = byteToInt(a); 10 | return intToHexString(a1); 11 | } 12 | 13 | public static String littleEndianBytesToHexString(byte a, byte b) { 14 | return intToHexString(littleEndianBytesToInt(a, b)); 15 | } 16 | 17 | public static String littleEndianBytesToHexString(byte a, byte b, byte c, byte d) { 18 | return intToHexString(littleEndianBytesToInt(a, b, c, d)); 19 | } 20 | 21 | public static String littleEndianBytesToHexString(byte[] value, int startPos, int length) { 22 | return intToHexString(littleEndianBytesToInt(value, startPos, length)); 23 | } 24 | 25 | public static int littleEndianBytesToInt(byte a, byte b) { 26 | int a1 = byteToInt(a); 27 | int b1 = byteToInt(b); 28 | return (b1 << 8) + a1; 29 | } 30 | 31 | public static int littleEndianBytesToInt(byte a, byte b, byte c, byte d) { 32 | int a1 = byteToInt(a); 33 | int b1 = byteToInt(b); 34 | int c1 = byteToInt(c); 35 | int d1 = byteToInt(d); 36 | return (d1 << 24) + (c1 << 16) + (b1 << 8) + a1; 37 | } 38 | 39 | public static int littleEndianBytesToInt(byte[] value, int startPos, int length) { 40 | if (length == 1) { 41 | return byteToInt(value[startPos]); 42 | } else if (length == 2) { 43 | return littleEndianBytesToInt(value[startPos], value[startPos + 1]); 44 | } else if (length == 4) { 45 | return littleEndianBytesToInt(value[startPos], value[startPos + 1], value[startPos + 2], value[startPos + 3]); 46 | } else { 47 | return 0; 48 | } 49 | } 50 | 51 | public static int byteToInt(byte a) { 52 | if (a < 0) { 53 | return a + 256; 54 | } 55 | return a; 56 | } 57 | 58 | public static String intToHexString(int a) { 59 | return "0x" + Integer.toHexString(a); 60 | } 61 | 62 | public static String getString(byte[] value, int startPos, int length) throws IOException { 63 | byte[] data = new byte[length]; 64 | if (data.length >= 0) System.arraycopy(value, startPos, data, 0, data.length); 65 | return new String(data, StandardCharsets.UTF_8); 66 | } 67 | 68 | public static int parseInt(String valueFor) { 69 | if (valueFor.startsWith("0x")) { 70 | return Integer.parseInt(valueFor.substring(2), 16); 71 | } else { 72 | return Integer.parseInt(valueFor, 10); 73 | } 74 | } 75 | 76 | } -------------------------------------------------------------------------------- /arsceditor/src/main/java/com/google/devrel/gmscore/tools/apk/arsc/PackageUtils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.google.devrel.gmscore.tools.apk.arsc; 18 | 19 | import java.nio.ByteBuffer; 20 | import java.nio.charset.Charset; 21 | import java.nio.charset.StandardCharsets; 22 | 23 | /** Provides utility methods for package names. */ 24 | public final class PackageUtils { 25 | 26 | public static final int PACKAGE_NAME_SIZE = 256; 27 | 28 | private PackageUtils() {} // Prevent instantiation 29 | 30 | /** 31 | * Reads the package name from the buffer and repositions the buffer to point directly after 32 | * the package name. 33 | * @param buffer The buffer containing the package name. 34 | * @param offset The offset in the buffer to read from. 35 | * @return The package name. 36 | */ 37 | public static String readPackageName(ByteBuffer buffer, int offset) { 38 | byte[] data = buffer.array(); 39 | int length = 0; 40 | // Look for the null terminator for the string instead of using the entire buffer. 41 | // It's UTF-16 so check 2 bytes at a time to see if its double 0. 42 | for (int i = offset; i < data.length && i < PACKAGE_NAME_SIZE + offset; i += 2) { 43 | if (data[i] == 0 && data[i + 1] == 0) { 44 | length = i - offset; 45 | break; 46 | } 47 | } 48 | Charset utf16 = StandardCharsets.UTF_16LE; 49 | String str = new String(data, offset, length, utf16); 50 | buffer.position(offset + PACKAGE_NAME_SIZE); 51 | return str; 52 | } 53 | 54 | /** 55 | * Writes the provided package name to the buffer in UTF-16. 56 | * @param buffer The buffer that will be written to. 57 | * @param packageName The package name that will be written to the buffer. 58 | */ 59 | public static void writePackageName(ByteBuffer buffer, String packageName) { 60 | byte[] nameBytes = packageName.getBytes(StandardCharsets.UTF_16LE); 61 | buffer.put(nameBytes, 0, Math.min(nameBytes.length, PACKAGE_NAME_SIZE)); 62 | if (nameBytes.length < PACKAGE_NAME_SIZE) { 63 | // pad out the remaining space with an empty array. 64 | buffer.put(new byte[PACKAGE_NAME_SIZE - nameBytes.length]); 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /arsceditor/src/main/java/com/google/devrel/gmscore/tools/apk/arsc/XmlResourceMapChunk.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.google.devrel.gmscore.tools.apk.arsc; 18 | 19 | import javax.annotation.Nullable; 20 | import java.io.DataOutput; 21 | import java.io.IOException; 22 | import java.nio.ByteBuffer; 23 | import java.util.ArrayList; 24 | import java.util.List; 25 | 26 | /** 27 | * Represents an XML resource map chunk. 28 | * 29 | *

This chunk maps attribute ids to the resource ids of the attribute resource that defines the 30 | * attribute (e.g. type, enum values, etc.). 31 | */ 32 | public class XmlResourceMapChunk extends Chunk { 33 | 34 | /** The size of a resource reference for {@code resources} in bytes. */ 35 | private static final int RESOURCE_SIZE = 4; 36 | 37 | /** 38 | * Contains a mapping of attributeID to resourceID. For example, the attributeID 2 refers to the 39 | * resourceID returned by {@code resources.get(2)}. 40 | */ 41 | private final List resources = new ArrayList<>(); 42 | 43 | protected XmlResourceMapChunk(ByteBuffer buffer, @Nullable Chunk parent) { 44 | super(buffer, parent); 45 | } 46 | 47 | @Override 48 | protected void init(ByteBuffer buffer) { 49 | super.init(buffer); 50 | resources.addAll(enumerateResources(buffer)); 51 | } 52 | 53 | private List enumerateResources(ByteBuffer buffer) { 54 | int resourceCount = (getOriginalChunkSize() - getHeaderSize()) / RESOURCE_SIZE; 55 | List result = new ArrayList<>(resourceCount); 56 | int offset = this.offset + getHeaderSize(); 57 | buffer.mark(); 58 | buffer.position(offset); 59 | 60 | for (int i = 0; i < resourceCount; ++i) { 61 | result.add(buffer.getInt()); 62 | } 63 | 64 | buffer.reset(); 65 | return result; 66 | } 67 | 68 | /** Returns the resource ID that this {@code attributeId} maps to. */ 69 | public BinaryResourceIdentifier getResourceId(int attributeId) { 70 | return BinaryResourceIdentifier.create(resources.get(attributeId)); 71 | } 72 | 73 | @Override 74 | protected Type getType() { 75 | return Type.XML_RESOURCE_MAP; 76 | } 77 | 78 | @Override 79 | protected void writePayload(DataOutput output, ByteBuffer header, boolean shrink) 80 | throws IOException { 81 | super.writePayload(output, header, shrink); 82 | for (Integer resource : resources) { 83 | output.writeInt(resource); 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /arsceditor/src/main/java/com/mrikso/arsceditor/gui/tree/ArscNode.java: -------------------------------------------------------------------------------- 1 | package com.mrikso.arsceditor.gui.tree; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | import com.google.devrel.gmscore.tools.apk.arsc.PackageChunk; 5 | import com.google.devrel.gmscore.tools.apk.arsc.StringPoolChunk; 6 | import com.google.devrel.gmscore.tools.apk.arsc.TypeChunk; 7 | import com.google.devrel.gmscore.tools.apk.arsc.TypeSpecChunk; 8 | 9 | import javax.swing.*; 10 | 11 | public class ArscNode extends JNode { 12 | private String name; 13 | private String packageName; 14 | private int id; 15 | private boolean isRootPackage; 16 | private StringPoolChunk stringPool; 17 | private PackageChunk packageChunk; 18 | private TypeSpecChunk typeSpec; 19 | private TypeChunk typeChunk; 20 | private String type; 21 | 22 | public ArscNode() { 23 | super(); 24 | this.name = null; 25 | this.packageName = null; 26 | } 27 | 28 | public ArscNode(String name) { 29 | super(name); 30 | this.name = name; 31 | this.packageName = null; 32 | } 33 | 34 | public ArscNode(String name, String packageName) { 35 | super(name); 36 | this.name = name; 37 | this.packageName = packageName; 38 | } 39 | 40 | @Override 41 | public Icon getIcon() { 42 | return null; 43 | } 44 | 45 | public void setName(String name) { 46 | this.name = name; 47 | } 48 | 49 | public void setPackageName(String packageName) { 50 | this.packageName = packageName; 51 | } 52 | 53 | public void setId(int id) { 54 | this.id = id; 55 | } 56 | 57 | public void setType(String type) { 58 | this.type = type; 59 | } 60 | 61 | public String getType() { 62 | return type; 63 | } 64 | 65 | public int getId() { 66 | return id; 67 | } 68 | 69 | @Override 70 | public @NotNull String getName() { 71 | return name; 72 | } 73 | 74 | @NotNull 75 | public String getPackageName() { 76 | return packageName; 77 | } 78 | 79 | public PackageChunk getPackageChunk() { 80 | return packageChunk; 81 | } 82 | 83 | public void setPackageChunk(PackageChunk packageChunk) { 84 | this.packageChunk = packageChunk; 85 | } 86 | 87 | public StringPoolChunk getStringPool() { 88 | return stringPool; 89 | } 90 | 91 | public void setStringPool(StringPoolChunk stringPool) { 92 | this.stringPool = stringPool; 93 | } 94 | 95 | public TypeSpecChunk getTypeSpec() { 96 | return typeSpec; 97 | } 98 | 99 | public void setTypeSpec(TypeSpecChunk typeSpec) { 100 | this.typeSpec = typeSpec; 101 | } 102 | 103 | public TypeChunk getTypeChunk() { 104 | return typeChunk; 105 | } 106 | 107 | public void setType(TypeChunk type) { 108 | this.typeChunk = type; 109 | } 110 | 111 | public boolean isRootPackage() { 112 | return isRootPackage; 113 | } 114 | 115 | public void setRootPackage(boolean rootPackage) { 116 | isRootPackage = rootPackage; 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /arsceditor/src/main/java/com/google/devrel/gmscore/tools/apk/arsc/ResourceTableChunk.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.google.devrel.gmscore.tools.apk.arsc; 18 | 19 | import com.google.common.base.Preconditions; 20 | 21 | import javax.annotation.Nullable; 22 | import java.nio.ByteBuffer; 23 | import java.util.Collection; 24 | import java.util.Collections; 25 | import java.util.HashMap; 26 | import java.util.Map; 27 | 28 | /** 29 | * Represents a resource table structure. Its sub-chunks contain: 30 | * 31 | *

    32 | *
  • A {@link StringPoolChunk} containing all string values in the entire resource table. It does 33 | * not, however, contain the names of entries or type identifiers. 34 | *
  • One or more {@link PackageChunk}. 35 | *
36 | */ 37 | public final class ResourceTableChunk extends ChunkWithChunks { 38 | 39 | /** A string pool containing all string resource values in the entire resource table. */ 40 | private StringPoolChunk stringPool; 41 | 42 | /** The packages contained in this resource table. */ 43 | private final Map packages = new HashMap<>(); 44 | 45 | protected ResourceTableChunk(ByteBuffer buffer, @Nullable Chunk parent) { 46 | super(buffer, parent); 47 | // packageCount. We ignore this, because we already know how many chunks we have. 48 | Preconditions.checkState(buffer.getInt() >= 0, "ResourceTableChunk package count was < 0."); 49 | } 50 | 51 | @Override 52 | protected void init(ByteBuffer buffer) { 53 | super.init(buffer); 54 | packages.clear(); 55 | for (Chunk chunk : getChunks().values()) { 56 | if (chunk instanceof PackageChunk) { 57 | PackageChunk packageChunk = (PackageChunk) chunk; 58 | packages.put(packageChunk.getPackageName(), packageChunk); 59 | } else if (chunk instanceof StringPoolChunk) { 60 | stringPool = (StringPoolChunk) chunk; 61 | } 62 | } 63 | Preconditions.checkNotNull(stringPool, "ResourceTableChunk must have a string pool."); 64 | } 65 | 66 | /** Returns the string pool containing all string resource values in the resource table. */ 67 | public StringPoolChunk getStringPool() { 68 | return stringPool; 69 | } 70 | 71 | /** Returns the package with the given {@code packageName}. Else, returns null. */ 72 | @Nullable 73 | public PackageChunk getPackage(String packageName) { 74 | return packages.get(packageName); 75 | } 76 | 77 | /** Returns the packages contained in this resource table. */ 78 | public Collection getPackages() { 79 | return Collections.unmodifiableCollection(packages.values()); 80 | } 81 | 82 | @Override 83 | protected Type getType() { 84 | return Type.TABLE; 85 | } 86 | 87 | @Override 88 | protected void writeHeader(ByteBuffer output) { 89 | super.writeHeader(output); 90 | output.putInt(packages.size()); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /arsceditor/src/main/java/com/mrikso/arsceditor/valueeditor/Converter.java: -------------------------------------------------------------------------------- 1 | package com.mrikso.arsceditor.valueeditor; 2 | 3 | import com.mrikso.arsceditor.util.HexUtil; 4 | import com.mrikso.arsceditor.util.ResourceHelper; 5 | import com.mrikso.arsceditor.util.ResourceHelper2; 6 | import com.mrikso.arsceditor.util.TypedValue; 7 | import org.jetbrains.annotations.NotNull; 8 | import org.jetbrains.annotations.Nullable; 9 | import com.google.devrel.gmscore.tools.apk.arsc.BinaryResourceValue; 10 | 11 | public class Converter { 12 | 13 | public static BinaryResourceValue convertValue(@Nullable String value, @NotNull ValueType valueType){ 14 | TypedValue typedValue = new TypedValue(); 15 | int size = 8; 16 | switch (valueType){ 17 | case TYPE_COLOR: 18 | BinaryResourceValue.Type type = ResourceHelper.getColorType(value); 19 | int data = ResourceHelper.getColor(value); 20 | return new BinaryResourceValue(size,type, data); 21 | case TYPE_FLOAT: 22 | ResourceHelper2.parseFloatAttribute(null, value, typedValue, false); 23 | return new BinaryResourceValue(size,BinaryResourceValue.Type.FLOAT, typedValue.data); 24 | case TYPE_BOOLEAN: 25 | int dataDec = -1; 26 | if ("true".equalsIgnoreCase(value)) { 27 | dataDec = 1; 28 | } else if ("false".equalsIgnoreCase(value)) { 29 | dataDec = 0; 30 | } 31 | return new BinaryResourceValue(size,BinaryResourceValue.Type.INT_BOOLEAN, dataDec); 32 | case TYPE_INT_DEC: 33 | return new BinaryResourceValue(size,BinaryResourceValue.Type.INT_DEC, convertInt(value)); 34 | case TYPE_INT_HEX: 35 | return new BinaryResourceValue(size,BinaryResourceValue.Type.INT_HEX, convertInt(value)); 36 | case TYPE_FRACTION: 37 | ResourceHelper2.parseFloatAttribute(null, value, typedValue, false); 38 | return new BinaryResourceValue(size,BinaryResourceValue.Type.FRACTION, typedValue.data); 39 | case TYPE_ATTRIBUTE: 40 | return new BinaryResourceValue(size,BinaryResourceValue.Type.ATTRIBUTE, 41 | HexUtil.parseInt(value.substring(1))); 42 | case TYPE_DIMENSION: 43 | ResourceHelper2.parseFloatAttribute(null, value, typedValue, true); 44 | return new BinaryResourceValue(size,BinaryResourceValue.Type.DIMENSION, typedValue.data); 45 | case TYPE_REFERENCE: 46 | return new BinaryResourceValue(size,BinaryResourceValue.Type.REFERENCE, 47 | HexUtil.parseInt(value.substring(1))); 48 | case TYPE_DYNAMIC_REFERENCE: 49 | return new BinaryResourceValue(size,BinaryResourceValue.Type.DYNAMIC_REFERENCE, 50 | HexUtil.parseInt(value.substring(1))); 51 | } 52 | return null; 53 | } 54 | 55 | 56 | private static int convertInt(String rawValue) { 57 | try { 58 | // Decode into long, because there are some large hex values in the android resource files 59 | // (e.g. config_notificationsBatteryLowARGB = 0xFFFF0000 in sdk 14). 60 | // Integer.decode() does not support large, i.e. negative values in hex numbers. 61 | // try parsing decimal number 62 | return (int) Long.parseLong(rawValue); 63 | } catch (NumberFormatException nfe) { 64 | // try parsing hex number 65 | return Long.decode(rawValue).intValue(); 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /arsceditor/src/main/java/com/mrikso/arsceditor/gui/tree/ResourceEntry.java: -------------------------------------------------------------------------------- 1 | package com.mrikso.arsceditor.gui.tree; 2 | 3 | import com.mrikso.arsceditor.valueeditor.ValueType; 4 | 5 | public class ResourceEntry extends JTableTreeNode { 6 | private static final long serialVersionUID = 1L; 7 | private String name; 8 | private String value; 9 | private String decodedName; 10 | private String decodedValue; 11 | private ValueType valueType; 12 | private int nameIndex, valueIndex, entryIndex, entryParentIndex = -1; 13 | private boolean isChildren =true; 14 | 15 | public ResourceEntry() { 16 | super(); 17 | } 18 | 19 | public ResourceEntry(String id) { 20 | super(id); 21 | } 22 | 23 | @Override 24 | public void setName(String name) { 25 | this.name = name; 26 | } 27 | 28 | @Override 29 | public String getName() { 30 | return name; 31 | } 32 | 33 | @Override 34 | public String getDecodedName() { 35 | return decodedName; 36 | } 37 | 38 | @Override 39 | public void setDecodedName(String name) { 40 | decodedName = name; 41 | } 42 | 43 | @Override 44 | public void setId(String id) { 45 | setUserObject(id); 46 | } 47 | 48 | @Override 49 | public String getId() { 50 | return (String) getUserObject(); 51 | } 52 | 53 | @Override 54 | public void setNameIndex(int index) { 55 | this.nameIndex = index; 56 | } 57 | 58 | @Override 59 | public int getNameIndex() { 60 | return nameIndex; 61 | } 62 | 63 | @Override 64 | public void setValueIndex(int index) { 65 | valueIndex = index; 66 | } 67 | 68 | @Override 69 | public int getValueIndex() { 70 | return valueIndex; 71 | } 72 | 73 | @Override 74 | public void setEntryIndex(int index) { 75 | this.entryIndex = index; 76 | } 77 | 78 | @Override 79 | public int getEntryIndex() { 80 | return entryIndex; 81 | } 82 | 83 | @Override 84 | public void setEntryChildrenIndex(int index) { 85 | this.entryParentIndex = index; 86 | } 87 | 88 | @Override 89 | public int getEntryChildrenIndex() { 90 | return entryParentIndex; 91 | } 92 | 93 | @Override 94 | public String getValue() { 95 | return value; 96 | } 97 | 98 | @Override 99 | public void setValue(String value) { 100 | this.value = value; 101 | } 102 | 103 | @Override 104 | public String getDecodeValue() { 105 | return decodedValue; 106 | } 107 | 108 | @Override 109 | public void setDecodedValue(String value) { 110 | decodedValue = value; 111 | } 112 | 113 | @Override 114 | public ValueType getValueType() { 115 | return valueType; 116 | } 117 | 118 | @Override 119 | public void setValueType(ValueType value) { 120 | valueType = value; 121 | } 122 | 123 | /*@Override 124 | public TypeChunk getTypeChunk() { 125 | return null; 126 | } 127 | 128 | @Override 129 | public void setTypeChunk(TypeChunk value) { 130 | 131 | } 132 | */ 133 | @Override 134 | public boolean isComplex() { 135 | return false; 136 | } 137 | 138 | @Override 139 | public void setIsComplex(boolean isComplex) { 140 | 141 | } 142 | 143 | @Override 144 | public boolean isChildren() { 145 | return true; 146 | } 147 | 148 | @Override 149 | public void setIsChildren(boolean isChildren) { 150 | 151 | } 152 | 153 | @Override 154 | public String toString() { 155 | return getName(); 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /arsceditor/src/main/java/com/google/devrel/gmscore/tools/apk/arsc/XmlNodeChunk.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.google.devrel.gmscore.tools.apk.arsc; 18 | 19 | import javax.annotation.Nullable; 20 | import java.nio.ByteBuffer; 21 | 22 | /** The common superclass for the various types of XML nodes. */ 23 | public abstract class XmlNodeChunk extends Chunk { 24 | 25 | /** The line number in the original source at which this node appeared. */ 26 | private final int lineNumber; 27 | 28 | /** A string reference of this node's comment. If this is -1, then there is no comment. */ 29 | private final int comment; 30 | 31 | protected XmlNodeChunk(ByteBuffer buffer, @Nullable Chunk parent) { 32 | super(buffer, parent); 33 | lineNumber = buffer.getInt(); 34 | comment = buffer.getInt(); 35 | } 36 | 37 | /** Returns true if this XML node contains a comment. Else, returns false. */ 38 | public boolean hasComment() { 39 | return comment != -1; 40 | } 41 | 42 | /** Returns the line number in the original source at which this node appeared. */ 43 | public int getLineNumber() { 44 | return lineNumber; 45 | } 46 | 47 | /** Returns the comment associated with this node, if any. Else, returns the empty string. */ 48 | public String getComment() { 49 | return getString(comment); 50 | } 51 | 52 | /** 53 | * An {@link XmlNodeChunk} does not know by itself what strings its indices reference. In order 54 | * to get the actual string, the first {@link XmlChunk} ancestor is found. The 55 | * {@link XmlChunk} ancestor should have a string pool which {@code index} references. 56 | * 57 | * @param index The index of the string. 58 | * @return String that the given {@code index} references, or empty string if {@code index} is -1. 59 | */ 60 | protected String getString(int index) { 61 | if (index == -1) { // Special case. Packed XML files use -1 for "no string entry" 62 | return ""; 63 | } 64 | Chunk parent = getParent(); 65 | while (parent != null) { 66 | if (parent instanceof XmlChunk) { 67 | return ((XmlChunk) parent).getString(index); 68 | } 69 | parent = parent.getParent(); 70 | } 71 | throw new IllegalStateException("XmlNodeChunk did not have an XmlChunk parent."); 72 | } 73 | 74 | /** 75 | * An {@link XmlNodeChunk} and anything that is itself an {@link XmlNodeChunk} has a header size 76 | * of 16. Anything else is, interestingly, considered to be a payload. For that reason, this 77 | * method is final. 78 | */ 79 | @Override 80 | protected final void writeHeader(ByteBuffer output) { 81 | super.writeHeader(output); 82 | output.putInt(lineNumber); 83 | output.putInt(comment); 84 | } 85 | 86 | /** 87 | * Returns a brief description of this XML node. The representation of this information is 88 | * subject to change, but below is a typical example: 89 | * 90 | *
"XmlNodeChunk{line=1234, comment=My awesome comment.}"
91 | */ 92 | @Override 93 | public String toString() { 94 | return String.format("XmlNodeChunk{line=%d, comment=%s}", getLineNumber(), getComment()); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /arsceditor/src/main/java/com/mrikso/arsceditor/gui/tree/ResourceDirectory.java: -------------------------------------------------------------------------------- 1 | package com.mrikso.arsceditor.gui.tree; 2 | 3 | import com.mrikso.arsceditor.valueeditor.ValueType; 4 | import com.google.devrel.gmscore.tools.apk.arsc.TypeChunk; 5 | 6 | public class ResourceDirectory extends JTableTreeNode { 7 | 8 | private static final long serialVersionUID = 1L; 9 | private String value; 10 | private String name; 11 | private String decodedName; 12 | private String decodedValue; 13 | private ValueType valueType; 14 | private TypeChunk typeChunk; 15 | private int nameIndex, valueIndex, entryIndex, entryParentIndex = -1; 16 | private boolean isComplex; 17 | 18 | public ResourceDirectory() { 19 | super(); 20 | } 21 | 22 | public ResourceDirectory(String id) { 23 | super(id); 24 | } 25 | 26 | @Override 27 | public void setName(String name) { 28 | this.name = name; 29 | } 30 | 31 | @Override 32 | public String getDecodedName() { 33 | return decodedName; 34 | } 35 | 36 | @Override 37 | public void setDecodedName(String name) { 38 | decodedName = name; 39 | } 40 | 41 | @Override 42 | public String getName() { 43 | return name; 44 | } 45 | 46 | @Override 47 | public void setId(String id) { 48 | setUserObject(id); 49 | } 50 | 51 | @Override 52 | public String getId() { 53 | return (String) getUserObject(); 54 | } 55 | 56 | @Override 57 | public void setNameIndex(int index) { 58 | this.nameIndex = index; 59 | } 60 | 61 | @Override 62 | public int getNameIndex() { 63 | return nameIndex; 64 | } 65 | 66 | @Override 67 | public void setValueIndex(int index) { 68 | valueIndex = index; 69 | } 70 | 71 | @Override 72 | public int getValueIndex() { 73 | return valueIndex; 74 | } 75 | 76 | @Override 77 | public void setEntryIndex(int index) { 78 | this.entryIndex = index; 79 | } 80 | 81 | @Override 82 | public int getEntryIndex() { 83 | return entryIndex; 84 | } 85 | 86 | @Override 87 | public void setEntryChildrenIndex(int index) { 88 | this.entryParentIndex = index; 89 | } 90 | 91 | @Override 92 | public int getEntryChildrenIndex() { 93 | return entryParentIndex; 94 | } 95 | 96 | @Override 97 | public String getValue() { 98 | return value; 99 | } 100 | 101 | @Override 102 | public void setValue(String value) { 103 | this.value = value; 104 | } 105 | 106 | @Override 107 | public String getDecodeValue() { 108 | return decodedValue; 109 | } 110 | 111 | @Override 112 | public void setDecodedValue(String value) { 113 | decodedValue = value; 114 | } 115 | 116 | @Override 117 | public ValueType getValueType() { 118 | return valueType; 119 | } 120 | 121 | @Override 122 | public void setValueType(ValueType value) { 123 | valueType = value; 124 | } 125 | 126 | /* @Override 127 | public TypeChunk getTypeChunk() { 128 | return typeChunk; 129 | } 130 | 131 | @Override 132 | public void setTypeChunk(TypeChunk value) { 133 | typeChunk = value; 134 | }*/ 135 | 136 | @Override 137 | public boolean isComplex() { 138 | return isComplex; 139 | } 140 | 141 | @Override 142 | public void setIsComplex(boolean isComplex) { 143 | this.isComplex = isComplex; 144 | } 145 | 146 | @Override 147 | public boolean isChildren() { 148 | return false; 149 | } 150 | 151 | @Override 152 | public void setIsChildren(boolean isChildren) { 153 | 154 | } 155 | } 156 | 157 | -------------------------------------------------------------------------------- /arsceditor/src/main/java/com/google/devrel/gmscore/tools/apk/arsc/TypeSpecChunk.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.google.devrel.gmscore.tools.apk.arsc; 18 | 19 | import com.google.common.base.Preconditions; 20 | import com.google.common.primitives.UnsignedBytes; 21 | 22 | import javax.annotation.Nullable; 23 | import java.io.DataOutput; 24 | import java.io.IOException; 25 | import java.nio.ByteBuffer; 26 | 27 | /** A chunk that contains a collection of resource entries for a particular resource data type. */ 28 | public final class TypeSpecChunk extends Chunk { 29 | 30 | /** The id of the resource type that this type spec refers to. */ 31 | private final int id; 32 | 33 | /** Resource configuration masks. */ 34 | private final int[] resources; 35 | 36 | protected TypeSpecChunk(ByteBuffer buffer, @Nullable Chunk parent) { 37 | super(buffer, parent); 38 | id = UnsignedBytes.toInt(buffer.get()); 39 | buffer.position(buffer.position() + 3); // Skip 3 bytes for packing 40 | int resourceCount = buffer.getInt(); 41 | resources = new int[resourceCount]; 42 | 43 | for (int i = 0; i < resourceCount; ++i) { 44 | resources[i] = buffer.getInt(); 45 | } 46 | } 47 | 48 | /** 49 | * Returns the (1-based) type id of the resources that this {@link TypeSpecChunk} has 50 | * configuration masks for. 51 | */ 52 | public int getId() { 53 | return id; 54 | } 55 | 56 | /** Returns the number of resource entries that this chunk has configuration masks for. */ 57 | public int getResourceCount() { 58 | return resources.length; 59 | } 60 | 61 | @Override 62 | protected Type getType() { 63 | return Type.TABLE_TYPE_SPEC; 64 | } 65 | 66 | /** Returns the name of the type this chunk represents (e.g. string, attr, id). */ 67 | public String getTypeName() { 68 | PackageChunk packageChunk = getPackageChunk(); 69 | Preconditions.checkNotNull(packageChunk, "%s has no parent package.", getClass()); 70 | StringPoolChunk typePool = packageChunk.getTypeStringPool(); 71 | Preconditions.checkNotNull(typePool, "%s's parent package has no type pool.", getClass()); 72 | return typePool.getString(getId() - 1); // - 1 here to convert to 0-based index 73 | } 74 | 75 | /** Returns the package enclosing this chunk, if any. Else, returns null. */ 76 | @Nullable 77 | private PackageChunk getPackageChunk() { 78 | Chunk chunk = getParent(); 79 | while (chunk != null && !(chunk instanceof PackageChunk)) { 80 | chunk = chunk.getParent(); 81 | } 82 | return chunk != null ? (PackageChunk) chunk : null; 83 | } 84 | 85 | @Override 86 | protected void writeHeader(ByteBuffer output) { 87 | super.writeHeader(output); 88 | // id is an unsigned byte in the range [0-255]. It is guaranteed to be non-negative. 89 | // Because our output is in little-endian, we are making use of the 4 byte packing here 90 | output.putInt(id); 91 | output.putInt(resources.length); 92 | } 93 | 94 | @Override 95 | protected void writePayload(DataOutput output, ByteBuffer header, boolean shrink) 96 | throws IOException { 97 | for (int resource : resources) { 98 | output.writeInt(resource); 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /arsceditor/src/main/java/com/google/devrel/gmscore/tools/apk/arsc/BinaryResourceIdentifier.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.google.devrel.gmscore.tools.apk.arsc; 18 | 19 | import com.google.common.base.Preconditions; 20 | 21 | import java.util.Objects; 22 | 23 | /** 24 | * Resources in a {@link ResourceTableChunk} are identified by an integer of the form 0xpptteeee, 25 | * where pp is the {@link PackageChunk} id, tt is the {@link TypeChunk} id, and eeee is the index of 26 | * the entry in the {@link TypeChunk}. 27 | */ 28 | public class BinaryResourceIdentifier { 29 | 30 | /** The {@link PackageChunk} id mask for a packed resource id of the form 0xpptteeee. */ 31 | private static final int PACKAGE_ID_MASK = 0xFF000000; 32 | private static final int PACKAGE_ID_SHIFT = 24; 33 | 34 | /** The {@link TypeChunk} id mask for a packed resource id of the form 0xpptteeee. */ 35 | private static final int TYPE_ID_MASK = 0x00FF0000; 36 | private static final int TYPE_ID_SHIFT = 16; 37 | 38 | /** The {@link TypeChunk.Entry} id mask for a packed resource id of the form 0xpptteeee. */ 39 | private static final int ENTRY_ID_MASK = 0xFFFF; 40 | private static final int ENTRY_ID_SHIFT = 0; 41 | 42 | private final int packageId; 43 | private final int typeId; 44 | private final int entryId; 45 | 46 | /** Returns a {@link BinaryResourceIdentifier} from a {@code resourceId} of the form 0xpptteeee. */ 47 | public static BinaryResourceIdentifier create(int resourceId) { 48 | int packageId = (resourceId & PACKAGE_ID_MASK) >>> PACKAGE_ID_SHIFT; 49 | int typeId = (resourceId & TYPE_ID_MASK) >>> TYPE_ID_SHIFT; 50 | int entryId = (resourceId & ENTRY_ID_MASK) >>> ENTRY_ID_SHIFT; 51 | return create(packageId, typeId, entryId); 52 | } 53 | 54 | /** Returns a {@link BinaryResourceIdentifier} with the given identifiers. */ 55 | public static BinaryResourceIdentifier create(int packageId, int typeId, int entryId) { 56 | Preconditions.checkState((packageId & 0xFF) == packageId, "packageId must be <= 0xFF."); 57 | Preconditions.checkState((typeId & 0xFF) == typeId, "typeId must be <= 0xFF."); 58 | Preconditions.checkState((entryId & 0xFFFF) == entryId, "entryId must be <= 0xFFFF."); 59 | return new BinaryResourceIdentifier(packageId, typeId, entryId); 60 | } 61 | 62 | private BinaryResourceIdentifier(int packageId, int typeId, int entryId) { 63 | this.packageId = packageId; 64 | this.typeId = typeId; 65 | this.entryId = entryId; 66 | } 67 | 68 | /** The (1-based) id of the {@link PackageChunk} containing this resource. */ 69 | public int packageId() { return packageId; } 70 | 71 | /** The (1-based) id of the {@link TypeChunk} containing this resource. */ 72 | public int typeId() { return typeId; } 73 | 74 | /** The (0-based) index of the entry in a {@link TypeChunk} containing this resource. */ 75 | public int entryId() { return entryId; } 76 | 77 | /** Returns resource id of the form 0xpptteeee. */ 78 | public int resourceId() { 79 | return packageId << PACKAGE_ID_SHIFT | typeId << TYPE_ID_SHIFT | entryId; 80 | } 81 | 82 | @Override 83 | public boolean equals(Object o) { 84 | if (this == o) return true; 85 | if (o == null || getClass() != o.getClass()) return false; 86 | BinaryResourceIdentifier that = (BinaryResourceIdentifier)o; 87 | return packageId == that.packageId && 88 | typeId == that.typeId && 89 | entryId == that.entryId; 90 | } 91 | 92 | @Override 93 | public int hashCode() { 94 | return Objects.hash(packageId, typeId, entryId); 95 | } 96 | 97 | @Override 98 | public String toString() { 99 | return String.format("0x%1$08x", resourceId()); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /arsceditor/src/main/java/com/mrikso/arsceditor/util/AttrNameHelper.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021 MrIkso 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU General Public License 15 | * along with this program. If not, see . 16 | */ 17 | 18 | package com.mrikso.arsceditor.util; 19 | 20 | import com.mrikso.arsceditor.model.ResId; 21 | import org.w3c.dom.Document; 22 | import org.w3c.dom.Element; 23 | import org.w3c.dom.Node; 24 | import org.w3c.dom.NodeList; 25 | import org.xml.sax.SAXException; 26 | 27 | import javax.xml.parsers.DocumentBuilder; 28 | import javax.xml.parsers.DocumentBuilderFactory; 29 | import javax.xml.parsers.ParserConfigurationException; 30 | import java.io.IOException; 31 | import java.io.InputStream; 32 | import java.util.HashMap; 33 | 34 | public class AttrNameHelper { 35 | 36 | private final HashMap attrAndroidMap = new HashMap<>(); 37 | 38 | private HashMap attrPackageMap = new HashMap<>(); 39 | 40 | public static final String PUBLIC_XML = "public-final.xml"; 41 | private static AttrNameHelper instance; 42 | 43 | public static AttrNameHelper getInstance() { 44 | if (instance == null) { 45 | instance = new AttrNameHelper(); 46 | return instance; 47 | } 48 | return instance; 49 | } 50 | 51 | public AttrNameHelper() { 52 | try { 53 | initPublicNameToResId(); 54 | } catch (IOException | ParserConfigurationException | SAXException e) { 55 | e.printStackTrace(); 56 | } 57 | } 58 | 59 | private void initPublicNameToResId() throws IOException, ParserConfigurationException, SAXException { 60 | DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); 61 | DocumentBuilder db = dbf.newDocumentBuilder(); 62 | ClassLoader classLoader = getClass().getClassLoader(); 63 | InputStream in = classLoader.getResourceAsStream(PUBLIC_XML); 64 | Document document = db.parse(in); 65 | document.normalize(); 66 | NodeList nodeList = document.getElementsByTagName("public"); 67 | for (int i = 0; i < nodeList.getLength(); i++) { 68 | Node node = nodeList.item(i); 69 | if (node.getNodeType() == Node.ELEMENT_NODE) { 70 | Element element = (Element) node; 71 | String type = element.getAttribute("type"); 72 | String name = element.getAttribute("name"); 73 | String id = element.getAttribute("id"); 74 | if (!id.isEmpty()) 75 | attrAndroidMap.put(Integer.decode(id), new ResId(Integer.decode(id), name, type)); 76 | } 77 | } 78 | } 79 | 80 | public void setAttrPackageMap(HashMap attrPackageMap) { 81 | this.attrPackageMap = attrPackageMap; 82 | } 83 | 84 | public String getName(int id, String pkg) { 85 | if (attrAndroidMap.containsKey(id)) { 86 | ResId resId = attrAndroidMap.get(id); 87 | if (resId.getType().equals("attr")) { 88 | return "android:" + resId.getName(); 89 | } else { 90 | return "android:" + resId.getType() + "/" + resId.getName(); 91 | } 92 | } else if (attrPackageMap.containsKey(id)) { 93 | ResId resId = attrPackageMap.get(id); 94 | if (resId.getType().equals("attr")) { 95 | return resId.getName(); 96 | } else { 97 | return resId.getType() + "/" + resId.getName(); 98 | } 99 | } 100 | return null; 101 | } 102 | 103 | /*public int getId(String name, String pkg) { 104 | if (!"android".equals(pkg)) { 105 | if (attrAndroidMap.containsValue(name)) { 106 | return attrAndroidMap.inverse().get(name); 107 | } 108 | } 109 | return 0; 110 | }*/ 111 | 112 | } 113 | -------------------------------------------------------------------------------- /arsceditor/src/main/java/com/google/devrel/gmscore/tools/apk/arsc/LibraryChunk.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.google.devrel.gmscore.tools.apk.arsc; 18 | 19 | import javax.annotation.Nullable; 20 | import java.io.DataOutput; 21 | import java.io.IOException; 22 | import java.nio.ByteBuffer; 23 | import java.nio.ByteOrder; 24 | import java.util.ArrayList; 25 | import java.util.List; 26 | import java.util.Objects; 27 | 28 | /** 29 | * Contains a list of package-id to package name mappings for any shared libraries used in this 30 | * {@link ResourceTableChunk}. The package-id's encoded in this resource table may be different 31 | * than the id's assigned at runtime 32 | */ 33 | public final class LibraryChunk extends Chunk { 34 | 35 | /** The number of resources of this type at creation time. */ 36 | private final int entryCount; 37 | 38 | /** The libraries used in this chunk (package id + name). */ 39 | private final List entries = new ArrayList<>(); 40 | 41 | protected LibraryChunk(ByteBuffer buffer, @Nullable Chunk parent) { 42 | super(buffer, parent); 43 | entryCount = buffer.getInt(); 44 | } 45 | 46 | @Override 47 | protected void init(ByteBuffer buffer) { 48 | super.init(buffer); 49 | entries.addAll(enumerateEntries(buffer)); 50 | } 51 | 52 | private List enumerateEntries(ByteBuffer buffer) { 53 | List result = new ArrayList<>(entryCount); 54 | int offset = this.offset + getHeaderSize(); 55 | int endOffset = offset + Entry.SIZE * entryCount; 56 | 57 | while (offset < endOffset) { 58 | result.add(Entry.create(buffer, offset)); 59 | offset += Entry.SIZE; 60 | } 61 | return result; 62 | } 63 | 64 | @Override 65 | protected Type getType() { 66 | return Type.TABLE_LIBRARY; 67 | } 68 | 69 | @Override 70 | protected void writeHeader(ByteBuffer output) { 71 | super.writeHeader(output); 72 | output.putInt(entries.size()); 73 | } 74 | 75 | @Override 76 | protected void writePayload(DataOutput output, ByteBuffer header, boolean shrink) 77 | throws IOException { 78 | for (Entry entry : entries) { 79 | output.write(entry.toByteArray(shrink)); 80 | } 81 | } 82 | 83 | /** A shared library package-id to package name entry. */ 84 | protected static class Entry implements SerializableResource { 85 | 86 | /** Library entries only contain a package ID (4 bytes) and a package name. */ 87 | private static final int SIZE = 4 + PackageUtils.PACKAGE_NAME_SIZE; 88 | 89 | private final int packageId; 90 | private final String packageName; 91 | 92 | static Entry create(ByteBuffer buffer, int offset) { 93 | int packageId = buffer.getInt(offset); 94 | String packageName = PackageUtils.readPackageName(buffer, offset + 4); 95 | return new Entry(packageId, packageName); 96 | } 97 | 98 | private Entry(int packageId, String packageName) { 99 | this.packageId = packageId; 100 | this.packageName = packageName; 101 | } 102 | 103 | /** The id assigned to the shared library at build time. */ 104 | public int packageId() { return packageId; } 105 | 106 | /** The package name of the shared library. */ 107 | public String packageName() { return packageName; } 108 | 109 | @Override 110 | public byte[] toByteArray() throws IOException { 111 | return toByteArray(false); 112 | } 113 | 114 | @Override 115 | public byte[] toByteArray(boolean shrink) throws IOException { 116 | ByteBuffer buffer = ByteBuffer.allocate(SIZE).order(ByteOrder.LITTLE_ENDIAN); 117 | buffer.putInt(packageId()); 118 | PackageUtils.writePackageName(buffer, packageName()); 119 | return buffer.array(); 120 | } 121 | 122 | @Override 123 | public boolean equals(Object o) { 124 | if (this == o) return true; 125 | if (o == null || getClass() != o.getClass()) return false; 126 | Entry entry = (Entry)o; 127 | return packageId == entry.packageId && 128 | Objects.equals(packageName, entry.packageName); 129 | } 130 | 131 | @Override 132 | public int hashCode() { 133 | return Objects.hash(packageId, packageName); 134 | } 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /arsceditor/src/main/java/com/mrikso/arsceditor/gui/treetable/JTreeTable.java: -------------------------------------------------------------------------------- 1 | package com.mrikso.arsceditor.gui.treetable; 2 | 3 | import javax.swing.*; 4 | import javax.swing.table.TableCellEditor; 5 | import javax.swing.table.TableCellRenderer; 6 | import javax.swing.tree.DefaultTreeSelectionModel; 7 | import javax.swing.tree.TreeModel; 8 | import java.awt.*; 9 | 10 | public class JTreeTable extends JTable { 11 | /** 12 | * 13 | */ 14 | private static final long serialVersionUID = 1L; 15 | protected TreeTableCellRenderer tree; 16 | 17 | public JTreeTable() { 18 | super(); 19 | init(); 20 | } 21 | 22 | public JTreeTable(TreeTableModel treeTableModel) { 23 | super(); 24 | init(); 25 | updateTable(treeTableModel); 26 | } 27 | 28 | private void init() { 29 | tree = new TreeTableCellRenderer(); 30 | // Force the JTable and JTree to share their row selection models. 31 | tree.setSelectionModel(new DefaultTreeSelectionModel() { 32 | /** 33 | * 34 | */ 35 | private static final long serialVersionUID = 1L; 36 | 37 | // Extend the implementation of the constructor, as if: 38 | /* public this() */ { 39 | setSelectionModel(listSelectionModel); 40 | } 41 | }); 42 | // Make the tree and table row heights the same. 43 | tree.setRowHeight(getRowHeight()); 44 | 45 | // Install the tree editor renderer and editor. 46 | setDefaultRenderer(TreeTableModel.class, tree); 47 | setDefaultEditor(TreeTableModel.class, new TreeTableCellEditor()); 48 | 49 | setShowGrid(false); 50 | setIntercellSpacing(new Dimension(0, 0)); 51 | } 52 | 53 | public void updateTable(TreeTableModel treeTableModel) { 54 | // Create the tree. It will be used as a renderer and editor. 55 | tree.setTreeModel(treeTableModel); 56 | // Install a tableModel representing the visible rows in the tree. 57 | super.setModel(new TreeTableModelAdapter(treeTableModel, tree)); 58 | 59 | } 60 | 61 | /* 62 | * Workaround for BasicTableUI anomaly. Make sure the UI never tries to 63 | * paint the editor. The UI currently uses different techniques to paint the 64 | * renderers and editors and overriding setBounds() below is not the right 65 | * thing to do for an editor. Returning -1 for the editing row in this case, 66 | * ensures the editor is never painted. 67 | */ 68 | public int getEditingRow() { 69 | return (getColumnClass(editingColumn) == TreeTableModel.class) ? -1 70 | : editingRow; 71 | } 72 | 73 | public TreeTableCellRenderer getTree() { 74 | return tree; 75 | } 76 | 77 | // 78 | // The renderer used to display the tree nodes, a JTree. 79 | // 80 | 81 | public class TreeTableCellRenderer extends JTree implements 82 | TableCellRenderer { 83 | 84 | /** 85 | * 86 | */ 87 | private static final long serialVersionUID = 1L; 88 | protected int visibleRow; 89 | protected TreeModel model; 90 | 91 | public TreeTableCellRenderer() { 92 | super(); 93 | 94 | } 95 | 96 | public TreeTableCellRenderer(TreeModel model) { 97 | super(model); 98 | this.model = model; 99 | } 100 | 101 | 102 | public void setTreeModel(TreeModel model) { 103 | this.model = model; 104 | setModel(model); 105 | } 106 | 107 | @Override 108 | public void setBounds(int x, int y, int w, int h) { 109 | super.setBounds(x, 0, w, JTreeTable.this.getHeight()); 110 | } 111 | 112 | @Override 113 | public void paint(Graphics g) { 114 | g.translate(0, -visibleRow * getRowHeight()); 115 | super.paint(g); 116 | } 117 | 118 | @Override 119 | public Component getTableCellRendererComponent(JTable table, 120 | Object value, boolean isSelected, boolean hasFocus, int row, 121 | int column) { 122 | if (isSelected) 123 | setBackground(table.getSelectionBackground()); 124 | else 125 | setBackground(table.getBackground()); 126 | 127 | visibleRow = row; 128 | return this; 129 | } 130 | } 131 | 132 | // 133 | // The editor used to interact with tree nodes, a JTree. 134 | // 135 | 136 | public class TreeTableCellEditor extends AbstractCellEditor implements 137 | TableCellEditor { 138 | public Component getTableCellEditorComponent(JTable table, 139 | Object value, boolean isSelected, int r, int c) { 140 | return tree; 141 | } 142 | } 143 | 144 | } 145 | -------------------------------------------------------------------------------- /arsceditor/src/main/java/com/google/devrel/gmscore/tools/apk/arsc/XmlAttribute.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.google.devrel.gmscore.tools.apk.arsc; 18 | 19 | import java.nio.ByteBuffer; 20 | import java.nio.ByteOrder; 21 | import java.util.Objects; 22 | 23 | /** Represents an XML attribute and value. */ 24 | public class XmlAttribute implements SerializableResource { 25 | 26 | /** The serialized size in bytes of an {@link XmlAttribute}. */ 27 | public static final int SIZE = 12 + BinaryResourceValue.SIZE; 28 | 29 | private final int namespaceIndex; 30 | private final int nameIndex; 31 | private final int rawValueIndex; 32 | private final BinaryResourceValue typedValue; 33 | private final XmlNodeChunk parent; 34 | 35 | /** 36 | * Creates a new {@link XmlAttribute} based on the bytes at the current {@code buffer} position. 37 | * 38 | * @param buffer A buffer whose position is at the start of a {@link XmlAttribute}. 39 | * @param parent The parent chunk that contains this attribute; used for string lookups. 40 | */ 41 | public static XmlAttribute create(ByteBuffer buffer, XmlNodeChunk parent) { 42 | int namespace = buffer.getInt(); 43 | int name = buffer.getInt(); 44 | int rawValue = buffer.getInt(); 45 | BinaryResourceValue typedValue = BinaryResourceValue.create(buffer); 46 | return new XmlAttribute(namespace, name, rawValue, typedValue, parent); 47 | } 48 | 49 | private XmlAttribute(int namespaceIndex, 50 | int nameIndex, 51 | int rawValueIndex, 52 | BinaryResourceValue typedValue, 53 | XmlNodeChunk parent) { 54 | this.namespaceIndex = namespaceIndex; 55 | this.nameIndex = nameIndex; 56 | this.rawValueIndex = rawValueIndex; 57 | this.typedValue = typedValue; 58 | this.parent = parent; 59 | } 60 | 61 | /** A string reference to the namespace URI, or -1 if not present. */ 62 | public int namespaceIndex() { 63 | return namespaceIndex; 64 | } 65 | 66 | /** A string reference to the attribute name. */ 67 | public int nameIndex() { 68 | return nameIndex; 69 | } 70 | 71 | /** A string reference to a string containing the character value. */ 72 | public int rawValueIndex() { 73 | return rawValueIndex; 74 | } 75 | 76 | /** A {@link BinaryResourceValue} instance containing the parsed value. */ 77 | public BinaryResourceValue typedValue() { 78 | return typedValue; 79 | } 80 | 81 | /** The parent of this XML attribute; used for dereferencing the namespace and name. */ 82 | public XmlNodeChunk parent() { 83 | return parent; 84 | } 85 | 86 | /** The namespace URI, or the empty string if not present. */ 87 | public final String namespace() { 88 | return getString(namespaceIndex()); 89 | } 90 | 91 | /** The attribute name, or the empty string if not present. */ 92 | public final String name() { 93 | return getString(nameIndex()); 94 | } 95 | 96 | /** The raw character value. */ 97 | public final String rawValue() { 98 | return getString(rawValueIndex()); 99 | } 100 | 101 | private String getString(int index) { 102 | return parent().getString(index); 103 | } 104 | 105 | @Override 106 | public byte[] toByteArray() { 107 | return toByteArray(false); 108 | } 109 | 110 | @Override 111 | public byte[] toByteArray(boolean shrink) { 112 | ByteBuffer buffer = ByteBuffer.allocate(SIZE).order(ByteOrder.LITTLE_ENDIAN); 113 | buffer.putInt(namespaceIndex()); 114 | buffer.putInt(nameIndex()); 115 | buffer.putInt(rawValueIndex()); 116 | buffer.put(typedValue().toByteArray(shrink)); 117 | return buffer.array(); 118 | } 119 | 120 | @Override 121 | public boolean equals(Object o) { 122 | if (this == o) return true; 123 | if (o == null || getClass() != o.getClass()) return false; 124 | XmlAttribute that = (XmlAttribute)o; 125 | return namespaceIndex == that.namespaceIndex && 126 | nameIndex == that.nameIndex && 127 | rawValueIndex == that.rawValueIndex && 128 | Objects.equals(typedValue, that.typedValue) && 129 | Objects.equals(parent, that.parent); 130 | } 131 | 132 | @Override 133 | public int hashCode() { 134 | return Objects.hash(namespaceIndex, nameIndex, rawValueIndex, typedValue, parent); 135 | } 136 | 137 | /** 138 | * Returns a brief description of this XML attribute. The representation of this information is 139 | * subject to change, but below is a typical example: 140 | * 141 | *
"XmlAttribute{namespace=foo, name=bar, value=1234}"
142 | */ 143 | @Override 144 | public String toString() { 145 | return String.format("XmlAttribute{namespace=%s, name=%s, value=%s}", 146 | namespace(), name(), rawValue()); 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /arsceditor/src/main/java/com/google/devrel/gmscore/tools/apk/arsc/BinaryResourceValue.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.google.devrel.gmscore.tools.apk.arsc; 18 | 19 | import com.google.common.base.Preconditions; 20 | import com.google.common.collect.ImmutableMap; 21 | import com.google.common.collect.ImmutableMap.Builder; 22 | import com.google.common.primitives.UnsignedBytes; 23 | 24 | import java.nio.ByteBuffer; 25 | import java.nio.ByteOrder; 26 | import java.util.Map; 27 | import java.util.Objects; 28 | 29 | /** Represents a single typed resource value. */ 30 | public class BinaryResourceValue implements SerializableResource { 31 | 32 | /** Resource type codes. */ 33 | public enum Type { 34 | /** {@code data} is either 0 (undefined) or 1 (empty). */ 35 | NULL(0x00), 36 | /** {@code data} holds a {@link ResourceTableChunk} entry reference. */ 37 | REFERENCE(0x01), 38 | /** {@code data} holds an attribute resource identifier. */ 39 | ATTRIBUTE(0x02), 40 | /** {@code data} holds an index into the containing resource table's string pool. */ 41 | STRING(0x03), 42 | /** {@code data} holds a single-precision floating point number. */ 43 | FLOAT(0x04), 44 | /** {@code data} holds a complex number encoding a dimension value, such as "100in". */ 45 | DIMENSION(0x05), 46 | /** {@code data} holds a complex number encoding a fraction of a container. */ 47 | FRACTION(0x06), 48 | /** {@code data} holds a dynamic {@link ResourceTableChunk} entry reference. */ 49 | DYNAMIC_REFERENCE(0x07), 50 | /** {@code data} holds a dynamic attribute resource identifier. */ 51 | DYNAMIC_ATTRIBUTE(0x08), 52 | /** {@code data} is a raw integer value of the form n..n. */ 53 | INT_DEC(0x10), 54 | /** {@code data} is a raw integer value of the form 0xn..n. */ 55 | INT_HEX(0x11), 56 | /** {@code data} is either 0 (false) or 1 (true). */ 57 | INT_BOOLEAN(0x12), 58 | /** {@code data} is a raw integer value of the form #aarrggbb. */ 59 | INT_COLOR_ARGB8(0x1c), 60 | /** {@code data} is a raw integer value of the form #rrggbb. */ 61 | INT_COLOR_RGB8(0x1d), 62 | /** {@code data} is a raw integer value of the form #argb. */ 63 | INT_COLOR_ARGB4(0x1e), 64 | /** {@code data} is a raw integer value of the form #rgb. */ 65 | INT_COLOR_RGB4(0x1f); 66 | 67 | private final byte code; 68 | 69 | private static final Map FROM_BYTE; 70 | 71 | static { 72 | Builder builder = ImmutableMap.builder(); 73 | for (Type type : values()) { 74 | builder.put(type.code(), type); 75 | } 76 | FROM_BYTE = builder.build(); 77 | } 78 | 79 | Type(int code) { 80 | this.code = UnsignedBytes.checkedCast(code); 81 | } 82 | 83 | public byte code() { 84 | return code; 85 | } 86 | 87 | public static Type fromCode(byte code) { 88 | return Preconditions.checkNotNull(FROM_BYTE.get(code), "Unknown resource type: %s", code); 89 | } 90 | } 91 | 92 | /** The serialized size in bytes of a {@link BinaryResourceValue}. */ 93 | public static final int SIZE = 8; 94 | 95 | private final int size; 96 | private final Type type; 97 | private final int data; 98 | 99 | public static BinaryResourceValue create(ByteBuffer buffer) { 100 | int size = (buffer.getShort() & 0xFFFF); 101 | buffer.get(); // Unused 102 | Type type = Type.fromCode(buffer.get()); 103 | int data = buffer.getInt(); 104 | return new BinaryResourceValue(size, type, data); 105 | } 106 | 107 | public BinaryResourceValue(int size, Type type, int data) { 108 | this.size = size; 109 | this.type = type; 110 | this.data = data; 111 | } 112 | 113 | /** The length in bytes of this value. */ 114 | public int size() { return size; } 115 | 116 | /** The raw data type of this value. */ 117 | public Type type() { return type; } 118 | 119 | /** The actual 4-byte value; interpretation of the value depends on {@code dataType}. */ 120 | public int data() { return data; } 121 | 122 | @Override 123 | public byte[] toByteArray() { 124 | return toByteArray(false); 125 | } 126 | 127 | @Override 128 | public byte[] toByteArray(boolean shrink) { 129 | ByteBuffer buffer = ByteBuffer.allocate(SIZE).order(ByteOrder.LITTLE_ENDIAN); 130 | buffer.putShort((short) size()); 131 | buffer.put((byte) 0); // Unused 132 | buffer.put(type().code()); 133 | buffer.putInt(data()); 134 | return buffer.array(); 135 | } 136 | 137 | @Override 138 | public boolean equals(Object o) { 139 | if (this == o) return true; 140 | if (o == null || getClass() != o.getClass()) return false; 141 | BinaryResourceValue that = (BinaryResourceValue)o; 142 | return size == that.size && 143 | data == that.data && 144 | type == that.type; 145 | } 146 | 147 | @Override 148 | public int hashCode() { 149 | return Objects.hash(size, type, data); 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /arsceditor/src/main/java/com/google/devrel/gmscore/tools/apk/arsc/XmlStartElementChunk.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.google.devrel.gmscore.tools.apk.arsc; 18 | 19 | import com.google.common.base.Preconditions; 20 | 21 | import javax.annotation.Nullable; 22 | import java.io.DataOutput; 23 | import java.io.IOException; 24 | import java.nio.ByteBuffer; 25 | import java.util.ArrayList; 26 | import java.util.Collections; 27 | import java.util.List; 28 | 29 | /** Represents the beginning of an XML node. */ 30 | public final class XmlStartElementChunk extends XmlNodeChunk { 31 | 32 | /** A string reference to the namespace URI, or -1 if not present. */ 33 | private final int namespace; 34 | 35 | /** A string reference to the element name that this chunk represents. */ 36 | private final int name; 37 | 38 | /** The offset to the start of the attributes payload. */ 39 | private final int attributeStart; 40 | 41 | /** The number of attributes in the original buffer. */ 42 | private final int attributeCount; 43 | 44 | /** The (0-based) index of the id attribute, or -1 if not present. */ 45 | private final int idIndex; 46 | 47 | /** The (0-based) index of the class attribute, or -1 if not present. */ 48 | private final int classIndex; 49 | 50 | /** The (0-based) index of the style attribute, or -1 if not present. */ 51 | private final int styleIndex; 52 | 53 | /** The XML attributes associated with this element. */ 54 | private final List attributes = new ArrayList<>(); 55 | 56 | protected XmlStartElementChunk(ByteBuffer buffer, @Nullable Chunk parent) { 57 | super(buffer, parent); 58 | namespace = buffer.getInt(); 59 | name = buffer.getInt(); 60 | attributeStart = (buffer.getShort() & 0xFFFF); 61 | int attributeSize = (buffer.getShort() & 0xFFFF); 62 | Preconditions.checkState(attributeSize == XmlAttribute.SIZE, 63 | "attributeSize is wrong size. Got %s, want %s", attributeSize, XmlAttribute.SIZE); 64 | attributeCount = (buffer.getShort() & 0xFFFF); 65 | 66 | // The following indices are 1-based and need to be adjusted. 67 | idIndex = (buffer.getShort() & 0xFFFF) - 1; 68 | classIndex = (buffer.getShort() & 0xFFFF) - 1; 69 | styleIndex = (buffer.getShort() & 0xFFFF) - 1; 70 | } 71 | 72 | @Override 73 | protected void init(ByteBuffer buffer) { 74 | super.init(buffer); 75 | attributes.addAll(enumerateAttributes(buffer)); 76 | } 77 | 78 | private List enumerateAttributes(ByteBuffer buffer) { 79 | List result = new ArrayList<>(attributeCount); 80 | int offset = this.offset + getHeaderSize() + attributeStart; 81 | int endOffset = offset + XmlAttribute.SIZE * attributeCount; 82 | buffer.mark(); 83 | buffer.position(offset); 84 | 85 | while (offset < endOffset) { 86 | result.add(XmlAttribute.create(buffer, this)); 87 | offset += XmlAttribute.SIZE; 88 | } 89 | 90 | buffer.reset(); 91 | return result; 92 | } 93 | 94 | /** Returns the namespace URI, or the empty string if not present. */ 95 | public String getNamespace() { 96 | return getString(namespace); 97 | } 98 | 99 | /** Returns the element name that this chunk represents. */ 100 | public String getName() { 101 | return getString(name); 102 | } 103 | 104 | /** Returns an unmodifiable list of this XML element's attributes. */ 105 | public List getAttributes() { 106 | return Collections.unmodifiableList(attributes); 107 | } 108 | 109 | @Override 110 | protected Type getType() { 111 | return Type.XML_START_ELEMENT; 112 | } 113 | 114 | @Override 115 | protected void writePayload(DataOutput output, ByteBuffer header, boolean shrink) 116 | throws IOException { 117 | super.writePayload(output, header, shrink); 118 | output.writeInt(namespace); 119 | output.writeInt(name); 120 | output.writeShort((short) XmlAttribute.SIZE); // attribute start 121 | output.writeShort((short) XmlAttribute.SIZE); 122 | output.writeShort((short) attributes.size()); 123 | output.writeShort((short) (idIndex + 1)); 124 | output.writeShort((short) (classIndex + 1)); 125 | output.writeShort((short) (styleIndex + 1)); 126 | for (XmlAttribute attribute : attributes) { 127 | output.write(attribute.toByteArray(shrink)); 128 | } 129 | } 130 | 131 | /** 132 | * Returns a brief description of this XML node. The representation of this information is 133 | * subject to change, but below is a typical example: 134 | * 135 | *
136 |    * "XmlStartElementChunk{line=1234, comment=My awesome comment., namespace=foo, name=bar, ...}"
137 |    * 
138 | */ 139 | @Override 140 | public String toString() { 141 | return String.format( 142 | "XmlStartElementChunk{line=%d, comment=%s, namespace=%s, name=%s, attributes=%s}", 143 | getLineNumber(), getComment(), getNamespace(), getName(), attributes.toString()); 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn ( ) { 37 | echo "$*" 38 | } 39 | 40 | die ( ) { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save ( ) { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /arsceditor/src/main/java/com/mrikso/arsceditor/model/ResourceModel.java: -------------------------------------------------------------------------------- 1 | package com.mrikso.arsceditor.model; 2 | 3 | import com.google.devrel.gmscore.tools.apk.arsc.*; 4 | import com.mrikso.arsceditor.gui.tree.ResourceDirectory; 5 | import com.mrikso.arsceditor.gui.tree.ResourceEntry; 6 | import com.mrikso.arsceditor.util.AttrNameHelper; 7 | import com.mrikso.arsceditor.util.DecodeGenUtils; 8 | import com.mrikso.arsceditor.valueeditor.FormatValue; 9 | import com.mrikso.arsceditor.valueeditor.ValueHelper; 10 | import com.mrikso.arsceditor.valueeditor.ValueType; 11 | import org.jetbrains.annotations.NotNull; 12 | 13 | import java.util.Map; 14 | 15 | public class ResourceModel { 16 | 17 | private final StringPoolChunk stringPool; 18 | private final PackageChunk packageChunk; 19 | private final TypeSpecChunk typeSpec; 20 | private final TypeChunk type; 21 | private final String tableName; 22 | private ResourceDirectory root; 23 | 24 | public ResourceModel(@NotNull StringPoolChunk stringPool, @NotNull PackageChunk packageChunk, @NotNull TypeSpecChunk typeSpec, 25 | @NotNull TypeChunk typeChunk, String tableName) { 26 | this.stringPool = stringPool; 27 | this.packageChunk = packageChunk; 28 | this.typeSpec = typeSpec; 29 | this.type = typeChunk; 30 | this.tableName = tableName; 31 | } 32 | 33 | public void readTable() { 34 | AttrNameHelper nameHelper = AttrNameHelper.getInstance(); 35 | root = new ResourceDirectory(String.format("Resource Table (%s)", tableName)); 36 | if (type != null) { 37 | for (Map.Entry entry : type.getEntries().entrySet()) { 38 | int entryIndex = entry.getKey(); 39 | BinaryResourceIdentifier id = BinaryResourceIdentifier.create(packageChunk.getId(), typeSpec.getId(), entry.getKey()); 40 | String idN = id.toString(); 41 | FormatValue valueNormal = null; 42 | BinaryResourceValue value = entry.getValue().value(); 43 | 44 | ResourceDirectory resourceDirectory = new ResourceDirectory(idN); 45 | resourceDirectory.setName(entry.getValue().key()); 46 | // set index in stringpool 47 | resourceDirectory.setNameIndex(entry.getValue().keyIndex()); 48 | 49 | // set index in typemap 50 | resourceDirectory.setEntryIndex(entryIndex); 51 | 52 | ValueHelper.setTypeChunk(type); 53 | 54 | if (value != null) { 55 | valueNormal = formatValue(value); 56 | } 57 | 58 | // add complex values (style, attr, plurals, array) 59 | if (entry.getValue().isComplex()) { 60 | resourceDirectory.setIsComplex(true); 61 | // get map sub values 62 | Map values = entry.getValue().values(); 63 | if (values != null) { 64 | for (Map.Entry valueEntry : values.entrySet()) { 65 | valueNormal = formatValue(valueEntry.getValue()); 66 | int key = valueEntry.getKey(); 67 | String idParent = String.format("0x%1$08x", key); 68 | 69 | ResourceEntry resourceEntry = new ResourceEntry(idParent); 70 | resourceEntry.setName(idParent); 71 | resourceEntry.setDecodedName(nameHelper.getName(key, packageChunk.getPackageName())); 72 | resourceEntry.setValue(valueNormal.getValue()); 73 | resourceEntry.setValueType(valueNormal.getValueType()); 74 | resourceEntry.setDecodedValue(valueNormal.getDecodedNamedValue()); 75 | // value index if value type is string 76 | resourceEntry.setValueIndex(valueNormal.getIndex()); 77 | 78 | // set parent index 79 | resourceEntry.setEntryIndex(entryIndex); 80 | // set children index 81 | resourceEntry.setEntryChildrenIndex(key); 82 | 83 | resourceDirectory.add(resourceEntry); 84 | } 85 | } 86 | } 87 | // set simple values (string, colors, xml, etc.) 88 | else { 89 | if (valueNormal != null) { 90 | // set value 91 | resourceDirectory.setValue(valueNormal.getValue()); 92 | // set decoded value by id2name 93 | resourceDirectory.setDecodedValue(valueNormal.getDecodedNamedValue()); 94 | // set value type(string, attr, color etc.) 95 | resourceDirectory.setValueType(valueNormal.getValueType()); 96 | // set value index 97 | resourceDirectory.setValueIndex(valueNormal.getIndex()); 98 | } 99 | } 100 | 101 | // add parent references if type is style 102 | if (type.getTypeName().equals("style")) { 103 | int parentId = entry.getValue().parentEntry(); 104 | if (parentId != 0) { 105 | resourceDirectory.setValueType(ValueType.TYPE_REFERENCE); 106 | resourceDirectory.setValue(String.format("@0x%1$08x", parentId)); 107 | String decodedParentStyle = nameHelper.getName(parentId, packageChunk.getPackageName()); 108 | if (decodedParentStyle != null) { 109 | resourceDirectory.setDecodedValue(String.format("@%s", decodedParentStyle)); 110 | } 111 | resourceDirectory.setValueIndex(-1); 112 | } 113 | } 114 | 115 | root.add(resourceDirectory); 116 | } 117 | } 118 | } 119 | 120 | @NotNull 121 | private FormatValue formatValue(@NotNull BinaryResourceValue value) { 122 | if (value.type() == BinaryResourceValue.Type.STRING) { 123 | return new FormatValue(ValueType.TYPE_STRING, stringPool.getString(value.data()), value.data()); 124 | } 125 | 126 | return DecodeGenUtils.formatValue(value, stringPool, packageChunk.getPackageName()); 127 | } 128 | 129 | public ResourceDirectory getRoot() { 130 | return root; 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /arsceditor/src/main/java/com/google/devrel/gmscore/tools/apk/arsc/BinaryResourceString.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.google.devrel.gmscore.tools.apk.arsc; 18 | 19 | import com.google.common.io.ByteArrayDataOutput; 20 | import com.google.common.io.ByteStreams; 21 | import com.google.common.primitives.UnsignedBytes; 22 | 23 | import java.nio.ByteBuffer; 24 | import java.nio.charset.Charset; 25 | 26 | import static java.nio.charset.StandardCharsets.UTF_16LE; 27 | import static java.nio.charset.StandardCharsets.UTF_8; 28 | 29 | /** Provides utilities to decode/encode a String packed in an arsc resource file. */ 30 | public final class BinaryResourceString { 31 | 32 | /** Type of {@link BinaryResourceString} to encode / decode. */ 33 | public enum Type { 34 | UTF8(UTF_8), 35 | UTF16(UTF_16LE); 36 | 37 | private final Charset charset; 38 | 39 | Type(Charset charset) { 40 | this.charset = charset; 41 | } 42 | 43 | public Charset charset() { 44 | return charset; 45 | } 46 | } 47 | 48 | private BinaryResourceString() {} // Private constructor 49 | 50 | /** 51 | * Given a buffer and an offset into the buffer, returns a String. The {@code offset} is the 52 | * 0-based byte offset from the start of the buffer where the string resides. This should be the 53 | * location in memory where the string's character count, followed by its byte count, and then 54 | * followed by the actual string is located. 55 | * 56 | *

Here's an example UTF-8-encoded string of ab©: 57 | *

 58 |    * 03 04 61 62 C2 A9 00
 59 |    * ^ Offset should be here
 60 |    * 
61 | * 62 | * @param buffer The buffer containing the string to decode. 63 | * @param offset Offset into the buffer where the string resides. 64 | * @param type The encoding type that the {@link BinaryResourceString} is encoded in. 65 | * @return The decoded string. 66 | */ 67 | public static String decodeString(ByteBuffer buffer, int offset, Type type) { 68 | int length; 69 | int characterCount = decodeLength(buffer, offset, type); 70 | offset += computeLengthOffset(characterCount, type); 71 | // UTF-8 strings have 2 lengths: the number of characters, and then the encoding length. 72 | // UTF-16 strings, however, only have 1 length: the number of characters. 73 | if (type == Type.UTF8) { 74 | length = decodeLength(buffer, offset, type); 75 | offset += computeLengthOffset(length, type); 76 | } else { 77 | length = characterCount * 2; 78 | } 79 | return new String(buffer.array(), offset, length, type.charset()); 80 | } 81 | 82 | /** 83 | * Encodes a string in either UTF-8 or UTF-16 and returns the bytes of the encoded string. 84 | * Strings are prefixed by 2 values. The first is the number of characters in the string. 85 | * The second is the encoding length (number of bytes in the string). 86 | * 87 | *

Here's an example UTF-8-encoded string of ab©: 88 | *

03 04 61 62 C2 A9 00
89 | * 90 | * @param str The string to be encoded. 91 | * @param type The encoding type that the {@link BinaryResourceString} should be encoded in. 92 | * @return The encoded string. 93 | */ 94 | public static byte[] encodeString(String str, Type type) { 95 | byte[] bytes = str.getBytes(type.charset()); 96 | // The extra 5 bytes is for metadata (character count + byte count) and the NULL terminator. 97 | ByteArrayDataOutput output = ByteStreams.newDataOutput(bytes.length + 5); 98 | encodeLength(output, str.length(), type); 99 | if (type == Type.UTF8) { // Only UTF-8 strings have the encoding length. 100 | encodeLength(output, bytes.length, type); 101 | } 102 | output.write(bytes); 103 | // NULL-terminate the string 104 | if (type == Type.UTF8) { 105 | output.write(0); 106 | } else { 107 | output.writeShort(0); 108 | } 109 | return output.toByteArray(); 110 | } 111 | 112 | private static void encodeLength(ByteArrayDataOutput output, int length, Type type) { 113 | if (length < 0) { 114 | output.write(0); 115 | return; 116 | } 117 | if (type == Type.UTF8) { 118 | if (length > 0x7F) { 119 | output.write(((length & 0x7F00) >> 8) | 0x80); 120 | } 121 | output.write(length & 0xFF); 122 | } else { // UTF-16 123 | // TODO(acornwall): Replace output with a little-endian output. 124 | if (length > 0x7FFF) { 125 | int highBytes = ((length & 0x7FFF0000) >> 16) | 0x8000; 126 | output.write(highBytes & 0xFF); 127 | output.write((highBytes & 0xFF00) >> 8); 128 | } 129 | int lowBytes = length & 0xFFFF; 130 | output.write(lowBytes & 0xFF); 131 | output.write((lowBytes & 0xFF00) >> 8); 132 | } 133 | } 134 | 135 | private static int computeLengthOffset(int length, Type type) { 136 | return (type == Type.UTF8 ? 1 : 2) * (length >= (type == Type.UTF8 ? 0x80 : 0x8000) ? 2 : 1); 137 | } 138 | 139 | private static int decodeLength(ByteBuffer buffer, int offset, Type type) { 140 | return type == Type.UTF8 ? decodeLengthUTF8(buffer, offset) : decodeLengthUTF16(buffer, offset); 141 | } 142 | 143 | private static int decodeLengthUTF8(ByteBuffer buffer, int offset) { 144 | // UTF-8 strings use a clever variant of the 7-bit integer for packing the string length. 145 | // If the first byte is >= 0x80, then a second byte follows. For these values, the length 146 | // is WORD-length in big-endian & 0x7FFF. 147 | int length = UnsignedBytes.toInt(buffer.get(offset)); 148 | if ((length & 0x80) != 0) { 149 | length = ((length & 0x7F) << 8) | UnsignedBytes.toInt(buffer.get(offset + 1)); 150 | } 151 | return length; 152 | } 153 | 154 | private static int decodeLengthUTF16(ByteBuffer buffer, int offset) { 155 | // UTF-16 strings use a clever variant of the 7-bit integer for packing the string length. 156 | // If the first word is >= 0x8000, then a second word follows. For these values, the length 157 | // is DWORD-length in big-endian & 0x7FFFFFFF. 158 | int length = (buffer.getShort(offset) & 0xFFFF); 159 | if ((length & 0x8000) != 0) { 160 | length = ((length & 0x7FFF) << 16) | (buffer.getShort(offset + 2) & 0xFFFF); 161 | } 162 | return length; 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /arsceditor/src/main/java/com/mrikso/arsceditor/util/DecodeGenUtils.java: -------------------------------------------------------------------------------- 1 | package com.mrikso.arsceditor.util; 2 | 3 | import com.google.devrel.gmscore.tools.apk.arsc.BinaryResourceValue; 4 | import com.google.devrel.gmscore.tools.apk.arsc.StringPoolChunk; 5 | import com.mrikso.arsceditor.valueeditor.FormatValue; 6 | import com.mrikso.arsceditor.valueeditor.ValueType; 7 | import org.jetbrains.annotations.NotNull; 8 | import org.jetbrains.annotations.Nullable; 9 | 10 | import java.text.NumberFormat; 11 | import java.util.Locale; 12 | 13 | public class DecodeGenUtils { 14 | 15 | public static String decodeComplex(int data, boolean isFraction) { 16 | double value = (data & TypedValue.COMPLEX_MANTISSA_MASK << TypedValue.COMPLEX_MANTISSA_SHIFT) 17 | * TypedValue.RADIX_MULTS[data >> TypedValue.COMPLEX_RADIX_SHIFT & TypedValue.COMPLEX_RADIX_MASK]; 18 | int unitType = data & TypedValue.COMPLEX_UNIT_MASK; 19 | String unit; 20 | if (isFraction) { 21 | value *= 100; 22 | switch (unitType) { 23 | case TypedValue.COMPLEX_UNIT_FRACTION: 24 | unit = "%"; 25 | break; 26 | case TypedValue.COMPLEX_UNIT_FRACTION_PARENT: 27 | unit = "%p"; 28 | break; 29 | 30 | default: 31 | unit = "?f" + Integer.toHexString(unitType); 32 | } 33 | } else { 34 | switch (unitType) { 35 | case TypedValue.COMPLEX_UNIT_PX: 36 | unit = "px"; 37 | break; 38 | case TypedValue.COMPLEX_UNIT_DIP: 39 | unit = "dp"; 40 | break; 41 | case TypedValue.COMPLEX_UNIT_SP: 42 | unit = "sp"; 43 | break; 44 | case TypedValue.COMPLEX_UNIT_PT: 45 | unit = "pt"; 46 | break; 47 | case TypedValue.COMPLEX_UNIT_IN: 48 | unit = "in"; 49 | break; 50 | case TypedValue.COMPLEX_UNIT_MM: 51 | unit = "mm"; 52 | break; 53 | 54 | default: 55 | unit = "?d" + Integer.toHexString(unitType); 56 | } 57 | } 58 | return doubleToString(value) + unit; 59 | } 60 | 61 | public static String doubleToString(double value) { 62 | if (Double.compare(value, Math.floor(value)) == 0 63 | && !Double.isInfinite(value)) { 64 | return Integer.toString((int) value); 65 | } 66 | // remove trailing zeroes 67 | NumberFormat f = NumberFormat.getInstance(Locale.ROOT); 68 | f.setMaximumFractionDigits(4); 69 | f.setMinimumIntegerDigits(1); 70 | return f.format(value); 71 | } 72 | 73 | public static String floatToString(float value) { 74 | return doubleToString(value); 75 | } 76 | 77 | 78 | public static FormatValue formatValue( 79 | @NotNull BinaryResourceValue resValue, @Nullable StringPoolChunk stringPool, String packageName) { 80 | int data = resValue.data(); 81 | String decoded; 82 | 83 | AttrNameHelper nameHelper = AttrNameHelper.getInstance(); 84 | //System.out.println(String.format("data: %d, size: %d", data, resValue.size())); 85 | switch (resValue.type()) { 86 | case NULL: 87 | return new FormatValue(ValueType.TYPE_STRING, null); 88 | case DYNAMIC_ATTRIBUTE: 89 | break; 90 | case DYNAMIC_REFERENCE: 91 | decoded = nameHelper.getName(data, packageName); 92 | if(decoded != null){ 93 | return new FormatValue(ValueType.TYPE_DYNAMIC_REFERENCE, String.format(Locale.US, "@0x%1$08x", data), 94 | String.format("@%s", decoded)); 95 | } 96 | return new FormatValue(ValueType.TYPE_DYNAMIC_REFERENCE, String.format(Locale.US, "@0x%1$08x", data)); 97 | case REFERENCE: 98 | decoded = nameHelper.getName(data, packageName); 99 | if(decoded != null){ 100 | return new FormatValue(ValueType.TYPE_REFERENCE, String.format(Locale.US, "@0x%1$08x", data), 101 | String.format("@%s", decoded)); 102 | } 103 | return new FormatValue(ValueType.TYPE_REFERENCE, String.format(Locale.US, "@0x%1$08x", data)); 104 | case ATTRIBUTE: 105 | decoded = nameHelper.getName(data, packageName); 106 | if(decoded != null){ 107 | return new FormatValue(ValueType.TYPE_ATTRIBUTE, String.format(Locale.US, "?0x%1$x", data), 108 | String.format("?%s", decoded)); 109 | } 110 | return new FormatValue(ValueType.TYPE_ATTRIBUTE, String.format(Locale.US, "?0x%1$x", data)); 111 | case STRING: 112 | if(stringPool != null && stringPool.getStringCount() < data){ 113 | return new FormatValue(ValueType.TYPE_STRING, stringPool.getString(data), data); 114 | }else { 115 | // reference string link 116 | decoded = nameHelper.getName(data, packageName); 117 | if(decoded !=null){ 118 | return new FormatValue(ValueType.TYPE_REFERENCE, String.format(Locale.US, "@0x%1$x", data), data, 119 | String.format("@%s", decoded)); 120 | } 121 | return new FormatValue(ValueType.TYPE_REFERENCE, String.format(Locale.US, "@0x%1$x", data), data); 122 | } 123 | case DIMENSION: 124 | return new FormatValue(ValueType.TYPE_DIMENSION, decodeComplex(data, false)); 125 | case FRACTION: 126 | return new FormatValue(ValueType.TYPE_FRACTION, decodeComplex(data, true)); 127 | case FLOAT: 128 | return new FormatValue(ValueType.TYPE_FLOAT, floatToString(Float.intBitsToFloat(data))); 129 | case INT_DEC: 130 | return new FormatValue(ValueType.TYPE_INT_DEC, Integer.toString(data)); 131 | case INT_HEX: 132 | return new FormatValue(ValueType.TYPE_INT_HEX, "0x" + Integer.toHexString(data)); 133 | case INT_BOOLEAN: 134 | return new FormatValue(ValueType.TYPE_BOOLEAN, data == 0 ? "false" : "true"); 135 | case INT_COLOR_ARGB8: 136 | return new FormatValue(ValueType.TYPE_COLOR, String.format("#%08x", data)); 137 | case INT_COLOR_RGB8: 138 | return new FormatValue(ValueType.TYPE_COLOR, String.format("#%06x", data & 0xFFFFFF)); 139 | case INT_COLOR_ARGB4: 140 | return new FormatValue(ValueType.TYPE_COLOR, String.format("#%04x", data & 0xFFFF)); 141 | case INT_COLOR_RGB4: 142 | return new FormatValue(ValueType.TYPE_COLOR, String.format("#%03x", data & 0xFFF)); 143 | } 144 | 145 | return new FormatValue(ValueType.TYPE_STRING, String.format("@res/0x%x", data)); 146 | } 147 | 148 | } 149 | -------------------------------------------------------------------------------- /arsceditor/src/main/java/com/mrikso/arsceditor/gui/treetable/TreeTableCellEditor.java: -------------------------------------------------------------------------------- 1 | /* 2 | * $Id: TreeTableCellEditor.java,v 1.5 2005/10/13 08:59:59 kleopatra Exp $ 3 | * 4 | * Copyright 2004 Sun Microsystems, Inc., 4150 Network Circle, 5 | * Santa Clara, California 95054, U.S.A. All rights reserved. 6 | * 7 | * This library is free software; you can redistribute it and/or 8 | * modify it under the terms of the GNU Lesser General Public 9 | * License as published by the Free Software Foundation; either 10 | * version 2.1 of the License, or (at your option) any later version. 11 | * 12 | * This library is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 15 | * Lesser General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU Lesser General Public 18 | * License along with this library; if not, write to the Free Software 19 | * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA 20 | */ 21 | 22 | package com.mrikso.arsceditor.gui.treetable; 23 | 24 | import java.util.EventObject; 25 | 26 | import java.awt.Component; 27 | import java.awt.Rectangle; 28 | import java.awt.event.MouseEvent; 29 | import javax.swing.DefaultCellEditor; 30 | import javax.swing.Icon; 31 | import javax.swing.JTable; 32 | import javax.swing.JTextField; 33 | import javax.swing.JTree; 34 | import javax.swing.tree.DefaultTreeCellRenderer; 35 | import javax.swing.tree.TreeCellRenderer; 36 | 37 | /** 38 | * An editor that can be used to edit the tree column. This extends 39 | * DefaultCellEditor and uses a JTextField (actually, TreeTableTextField) 40 | * to perform the actual editing. 41 | *

To support editing of the tree column we can not make the tree 42 | * editable. The reason this doesn't work is that you can not use 43 | * the same component for editing and renderering. The table may have 44 | * the need to paint cells, while a cell is being edited. If the same 45 | * component were used for the rendering and editing the component would 46 | * be moved around, and the contents would change. When editing, this 47 | * is undesirable, the contents of the text field must stay the same, 48 | * including the caret blinking, and selections persisting. For this 49 | * reason the editing is done via a TableCellEditor. 50 | *

Another interesting thing to be aware of is how tree positions 51 | * its render and editor. The render/editor is responsible for drawing the 52 | * icon indicating the type of node (leaf, branch...). The tree is 53 | * responsible for drawing any other indicators, perhaps an additional 54 | * +/- sign, or lines connecting the various nodes. So, the renderer 55 | * is positioned based on depth. On the other hand, table always makes 56 | * its editor fill the contents of the cell. To get the allusion 57 | * that the table cell editor is part of the tree, we don't want the 58 | * table cell editor to fill the cell bounds. We want it to be placed 59 | * in the same manner as tree places it editor, and have table message 60 | * the tree to paint any decorations the tree wants. Then, we would 61 | * only have to worry about the editing part. The approach taken 62 | * here is to determine where tree would place the editor, and to override 63 | * the reshape method in the JTextField component to 64 | * nudge the textfield to the location tree would place it. Since 65 | * JXTreeTable will paint the tree behind the editor everything should 66 | * just work. So, that is what we are doing here. Determining of 67 | * the icon position will only work if the TreeCellRenderer is 68 | * an instance of DefaultTreeCellRenderer. If you need custom 69 | * TreeCellRenderers, that don't descend from DefaultTreeCellRenderer, 70 | * and you want to support editing in JXTreeTable, you will have 71 | * to do something similiar. 72 | * 73 | * @author Scott Violet 74 | * @author Ramesh Gupta 75 | */ 76 | public class TreeTableCellEditor extends DefaultCellEditor { 77 | public TreeTableCellEditor(JTree tree) { 78 | super(new TreeTableTextField()); 79 | if (tree == null) { 80 | throw new IllegalArgumentException("null tree"); 81 | } 82 | 83 | this.tree = tree; // immutable 84 | } 85 | 86 | /** 87 | * Overriden to determine an offset that tree would place the 88 | * editor at. The offset is determined from the 89 | * getRowBounds JTree method, and additionaly 90 | * from the icon DefaultTreeCellRenderer will use. 91 | *

The offset is then set on the TreeTableTextField component 92 | * created in the constructor, and returned. 93 | */ 94 | public Component getTableCellEditorComponent(JTable table, Object value, 95 | boolean isSelected, int row, 96 | int column) { 97 | Component component = super.getTableCellEditorComponent(table, value, 98 | isSelected, row, column); 99 | Rectangle bounds = tree.getRowBounds(row); 100 | int offset = bounds.x; 101 | TreeCellRenderer tcr = tree.getCellRenderer(); 102 | if (tcr instanceof DefaultTreeCellRenderer) { 103 | Object node = tree.getPathForRow(row).getLastPathComponent(); 104 | Icon icon; 105 | if (tree.getModel().isLeaf(node)) 106 | icon = ((DefaultTreeCellRenderer) tcr).getLeafIcon(); 107 | else if (tree.isExpanded(row)) 108 | icon = ((DefaultTreeCellRenderer) tcr).getOpenIcon(); 109 | else 110 | icon = ((DefaultTreeCellRenderer) tcr).getClosedIcon(); 111 | 112 | if (icon != null) { 113 | offset += ((DefaultTreeCellRenderer) tcr).getIconTextGap() + 114 | icon.getIconWidth(); 115 | } 116 | } 117 | ((TreeTableTextField) getComponent()).offset = offset; 118 | return component; 119 | } 120 | 121 | /** 122 | * This is overriden to forward the event to the tree. This will 123 | * return true if the click count >= clickCountToStart, or the event is null. 124 | */ 125 | public boolean isCellEditable(EventObject e) { 126 | if (e == null) { 127 | return true; 128 | } 129 | else if (e instanceof MouseEvent) { 130 | return (((MouseEvent) e).getClickCount() >= clickCountToStart); 131 | } 132 | 133 | // e is some other type of event... 134 | return false; 135 | } 136 | 137 | /** 138 | * Component used by TreeTableCellEditor. The only thing this does 139 | * is to override the reshape method, and to ALWAYS 140 | * make the x location be offset. 141 | */ 142 | static class TreeTableTextField extends JTextField { 143 | int offset; // changed to package private instead of public 144 | 145 | public void reshape(int x, int y, int width, int height) { 146 | // Allows precise positioning of text field in the tree cell. 147 | //Border border = this.getBorder(); // get this text field's border 148 | //Insets insets = border == null ? null : border.getBorderInsets(this); 149 | //int newOffset = offset - (insets == null ? 0 : insets.left); 150 | int newOffset = offset - getInsets().left; 151 | super.reshape(x + newOffset, y, width - newOffset, height); 152 | } 153 | } 154 | 155 | private final JTree tree; // immutable 156 | } -------------------------------------------------------------------------------- /arsceditor/src/main/java/com/mrikso/arsceditor/gui/tree/ArscTableView.java: -------------------------------------------------------------------------------- 1 | package com.mrikso.arsceditor.gui.tree; 2 | 3 | import com.mrikso.arsceditor.gui.MainWindow; 4 | import com.mrikso.arsceditor.gui.dialogs.ErrorDialog; 5 | import com.mrikso.arsceditor.gui.dialogs.ResourceEditDialog; 6 | import com.mrikso.arsceditor.gui.treetable.JTreeTable; 7 | import com.mrikso.arsceditor.gui.treetable.TreeTableModel; 8 | import com.mrikso.arsceditor.intrefaces.TableChangedListener; 9 | import com.mrikso.arsceditor.valueeditor.Converter; 10 | import com.mrikso.arsceditor.valueeditor.ValueHelper; 11 | import com.mrikso.arsceditor.valueeditor.ValueType; 12 | import org.jetbrains.annotations.Nullable; 13 | import com.google.devrel.gmscore.tools.apk.arsc.BinaryResourceValue; 14 | import com.google.devrel.gmscore.tools.apk.arsc.ResourceTableChunk; 15 | import com.google.devrel.gmscore.tools.apk.arsc.StringPoolChunk; 16 | import com.google.devrel.gmscore.tools.apk.arsc.TypeChunk; 17 | 18 | import javax.swing.*; 19 | import javax.swing.table.DefaultTableCellRenderer; 20 | import javax.swing.tree.TreePath; 21 | import java.awt.*; 22 | import java.awt.event.MouseEvent; 23 | import java.awt.event.MouseListener; 24 | 25 | public class ArscTableView extends JTreeTable implements MouseListener, ResourceEditDialog.ValueChangedListener { 26 | 27 | protected final MainWindow mainWindow; 28 | protected TreeTableModel treeTableModel; 29 | private TableChangedListener tableChangedListener; 30 | private JTableTreeNode selectedNode; 31 | 32 | public ArscTableView(MainWindow window) { 33 | super(); 34 | this.mainWindow = window; 35 | setDefaultRenderer(Object.class, new ArscStringRenderer()); 36 | addMouseListener(this); 37 | getTree().setCellRenderer(new ArscTreeCellRenderer()); 38 | } 39 | 40 | public void setTableChangedListener(TableChangedListener tableChangedListener) { 41 | this.tableChangedListener = tableChangedListener; 42 | } 43 | 44 | public void setTreeTableModel(TreeTableModel treeTableModel) { 45 | this.treeTableModel = treeTableModel; 46 | } 47 | 48 | public void updateTable() { 49 | updateTable(treeTableModel); 50 | } 51 | 52 | @Override 53 | public void mouseClicked(MouseEvent e) { 54 | Point p = e.getPoint(); 55 | int col = this.columnAtPoint(p); 56 | int row = this.rowAtPoint(p); 57 | if (row >= 0) { 58 | /* if (SwingUtilities.isLeftMouseButton(e) && e.getClickCount() == 2) { 59 | DefaultMutableTreeNode selectedNode = getSelectedNode(); 60 | if (selectedNode != null) { 61 | if (selectedNode instanceof ResourceModel.ResourceDirectory) { 62 | ResourceModel.ResourceDirectory resourceDirectory = (ResourceModel.ResourceDirectory) selectedNode; 63 | System.out.println(resourceDirectory.getName()); 64 | } else if (selectedNode instanceof ResourceModel.ResourceEntry) { 65 | ResourceModel.ResourceEntry resourceEntry = (ResourceModel.ResourceEntry) selectedNode; 66 | System.out.println(resourceEntry.getName()); 67 | } 68 | } 69 | 70 | } else*/ 71 | if (SwingUtilities.isRightMouseButton(e)) { 72 | getTree().setSelectionRow(row); 73 | selectedNode = getSelectedNode(); 74 | if (selectedNode != null && !selectedNode.isRoot()) { 75 | ResourceEditDialog resourceEditDialog = new ResourceEditDialog(mainWindow, selectedNode); 76 | resourceEditDialog.setValueChangedListener(this); 77 | resourceEditDialog.showDialog(); 78 | } 79 | // popupMenu.show(tree, e.getX(), e.getY()); 80 | }} 81 | } 82 | 83 | @Override 84 | public void mousePressed(MouseEvent e) { 85 | 86 | } 87 | 88 | @Override 89 | public void mouseReleased(MouseEvent e) { 90 | 91 | } 92 | 93 | @Override 94 | public void mouseEntered(MouseEvent e) { 95 | 96 | } 97 | 98 | @Override 99 | public void mouseExited(MouseEvent e) { 100 | 101 | } 102 | 103 | @Nullable 104 | public JTableTreeNode getSelectedNode() { 105 | TreePath path = getTree().getSelectionPath(); 106 | if (path != null) { 107 | Object component = path.getLastPathComponent(); 108 | return component instanceof JTableTreeNode ? (JTableTreeNode) component : null; 109 | } 110 | return null; 111 | } 112 | 113 | @Override 114 | public void onResourceEdited(String name, String id, String value, ValueType resourceType, boolean isChildren) { 115 | try { 116 | selectedNode.setId(id); 117 | selectedNode.setName(name); 118 | selectedNode.setValue(value); 119 | selectedNode.setValueType(resourceType); 120 | 121 | StringPoolChunk poolChunk = ValueHelper.getStringPoolChunk(); 122 | 123 | int index = selectedNode.getEntryIndex(); 124 | 125 | TypeChunk.Entry entry = ValueHelper.getTypeChunk().getEntries().get(index); 126 | 127 | // editing children node on complex value 128 | if (isChildren) { 129 | if (resourceType == ValueType.TYPE_STRING) { 130 | if(value.equals("null")){ 131 | BinaryResourceValue resourceValue = new BinaryResourceValue(8, BinaryResourceValue.Type.NULL, 1); 132 | entry.updateValue(selectedNode.getEntryChildrenIndex(), resourceValue); 133 | }else { 134 | // update the string value 135 | poolChunk.updateString(selectedNode.getValueIndex(), value); 136 | 137 | // update the name 138 | //entry.setKey(selectedNode.getNameIndex(), name); 139 | } 140 | } else { 141 | BinaryResourceValue resourceValue = Converter.convertValue(value, resourceType); 142 | entry.updateValue(selectedNode.getEntryChildrenIndex(), resourceValue); 143 | } 144 | } else { 145 | if (resourceType == ValueType.TYPE_STRING) { 146 | // update the string value 147 | poolChunk.updateString(selectedNode.getValueIndex(), value); 148 | 149 | // update the name 150 | entry.updateKey(selectedNode.getNameIndex(), name); 151 | } else if (selectedNode.isComplex()) { 152 | // update the name in complex values 153 | entry.updateKey(selectedNode.getNameIndex(), name); 154 | } else { 155 | // not complex 156 | BinaryResourceValue resourceValue = Converter.convertValue(value, resourceType); 157 | entry.updateValue(resourceValue); 158 | } 159 | } 160 | 161 | // update the entry 162 | ValueHelper.getTypeChunk().overrideEntry(index, entry); 163 | if (tableChangedListener != null) 164 | tableChangedListener.tableChanged(); 165 | }catch (Exception ex){ 166 | ex.printStackTrace(); 167 | new ErrorDialog(mainWindow, ex); 168 | } 169 | } 170 | 171 | static class ArscStringRenderer extends DefaultTableCellRenderer { 172 | private static final long serialVersionUID = 1L; 173 | 174 | public ArscStringRenderer() { 175 | super(); 176 | } 177 | 178 | @Override 179 | public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) { 180 | super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column); 181 | if (value != null) { 182 | setText(value.toString()); 183 | } else { 184 | setText(null); 185 | } 186 | return this; 187 | } 188 | 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /arsceditor/src/main/java/com/mrikso/arsceditor/gui/MainWindow.java: -------------------------------------------------------------------------------- 1 | package com.mrikso.arsceditor.gui; 2 | 3 | import com.google.common.io.Files; 4 | import com.mrikso.arsceditor.gui.dialogs.AboutDialog; 5 | import com.mrikso.arsceditor.gui.dialogs.ErrorDialog; 6 | import com.mrikso.arsceditor.gui.tree.ArscTableView; 7 | import com.mrikso.arsceditor.gui.tree.ArscTreeView; 8 | import com.mrikso.arsceditor.intrefaces.TableChangedListener; 9 | import com.mrikso.arsceditor.valueeditor.ArscWriter; 10 | import com.mrikso.arsceditor.valueeditor.ValueHelper; 11 | import com.google.devrel.gmscore.tools.apk.arsc.BinaryResourceFile; 12 | import com.google.devrel.gmscore.tools.apk.arsc.Chunk; 13 | 14 | import javax.swing.*; 15 | import javax.swing.filechooser.FileNameExtensionFilter; 16 | import java.awt.*; 17 | import java.awt.event.InputEvent; 18 | import java.awt.event.KeyEvent; 19 | import java.io.File; 20 | import java.net.URI; 21 | import java.nio.file.Paths; 22 | import java.util.List; 23 | 24 | public class MainWindow extends JFrame implements TableChangedListener { 25 | 26 | private ArscTreeView treeView; 27 | private JSplitPane splitPane; 28 | private JMenuItem saveAs; 29 | private String openedFilePath; 30 | 31 | public MainWindow() { 32 | loadComponent(); 33 | } 34 | 35 | protected void loadComponent() { 36 | ArscTableView arscTableView = new ArscTableView(this); 37 | arscTableView.setTableChangedListener(this); 38 | 39 | treeView = new ArscTreeView(this, null, arscTableView); 40 | treeView.setTableChangedListener(this); 41 | 42 | BorderLayout layout = new BorderLayout(); 43 | JPanel panel = new JPanel(); 44 | panel.setLayout(layout); 45 | 46 | splitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, new JScrollPane(treeView), new JScrollPane(arscTableView)); 47 | 48 | initSplitPane(); 49 | 50 | panel.add(splitPane, BorderLayout.CENTER); 51 | 52 | this.setJMenuBar(createMenuBar()); 53 | this.getContentPane().add(panel); 54 | 55 | this.setPreferredSize(new Dimension(900, 600)); 56 | this.setMinimumSize(new Dimension(600, 600)); 57 | this.setLocationRelativeTo(null); 58 | this.setTitle("Arsc Editor"); 59 | this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); 60 | this.pack(); 61 | this.setVisible(true); 62 | } 63 | 64 | /** 65 | * Creates a menu bar. 66 | */ 67 | protected JMenuBar createMenuBar() { 68 | JMenu fileMenu = new JMenu("File"); 69 | JMenuItem menuItem; 70 | 71 | menuItem = new JMenuItem("Open"); 72 | menuItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_O, 73 | InputEvent.CTRL_MASK)); 74 | menuItem.addActionListener(ae -> { 75 | JFileChooser fileChooser = new JFileChooser(); 76 | fileChooser.setAcceptAllFileFilterUsed(false); 77 | FileNameExtensionFilter filter = new FileNameExtensionFilter("Android resource files", "arsc"); 78 | fileChooser.addChoosableFileFilter(filter); 79 | int result = fileChooser.showOpenDialog(getRootPane()); 80 | 81 | if (result == JFileChooser.APPROVE_OPTION) { 82 | openedFilePath = fileChooser.getSelectedFile().getPath(); 83 | openFile(openedFilePath); 84 | } 85 | }); 86 | 87 | saveAs = new JMenuItem("Save"); 88 | saveAs.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_S, 89 | InputEvent.CTRL_MASK)); 90 | saveAs.setEnabled(false); 91 | saveAs.addActionListener(l -> { 92 | selectPathToSave(); 93 | }); 94 | 95 | fileMenu.add(menuItem); 96 | fileMenu.addSeparator(); 97 | fileMenu.add(saveAs); 98 | 99 | menuItem = new JMenuItem("Exit"); 100 | menuItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_X, 101 | InputEvent.CTRL_MASK)); 102 | menuItem.addActionListener(ae -> System.exit(0)); 103 | fileMenu.add(menuItem); 104 | 105 | JMenu aboutMenu = new JMenu("About"); 106 | menuItem = new JMenuItem("Open source code"); 107 | menuItem.addActionListener(l -> gitHomepage()); 108 | aboutMenu.add(menuItem); 109 | aboutMenu.addSeparator(); 110 | menuItem = new JMenuItem("Info"); 111 | menuItem.addActionListener(l -> new AboutDialog(MainWindow.this).showDialog()); 112 | aboutMenu.add(menuItem); 113 | 114 | // Create a menu bar 115 | JMenuBar menuBar = new JMenuBar(); 116 | 117 | menuBar.add(fileMenu); 118 | menuBar.add(aboutMenu); 119 | 120 | return menuBar; 121 | } 122 | 123 | private void gitHomepage() { 124 | try { 125 | Desktop.getDesktop().browse(new URI("https://github.com/MrIkso/ArscEditor")); 126 | } catch (Exception ex) { 127 | JOptionPane.showMessageDialog( 128 | this, 129 | ex.getMessage(), this.getTitle(), 130 | JOptionPane.WARNING_MESSAGE); 131 | } 132 | } 133 | 134 | private void initSplitPane() { 135 | splitPane.setDividerLocation(200); 136 | splitPane.setResizeWeight(0); 137 | Dimension minimumSize = new Dimension(200, 600); 138 | splitPane.getLeftComponent().setMinimumSize(minimumSize); 139 | splitPane.getRightComponent().setMinimumSize(minimumSize); 140 | } 141 | 142 | @Override 143 | public void tableChanged() { 144 | saveAs.setEnabled(true); 145 | } 146 | 147 | private void openFile(String path) { 148 | new SwingWorker, Chunk>() { 149 | @Override 150 | protected List doInBackground() throws Exception { 151 | byte[] resContents = 152 | java.nio.file.Files.readAllBytes(Paths.get(path)); 153 | BinaryResourceFile binaryRes = new BinaryResourceFile(resContents); 154 | return binaryRes.getChunks(); 155 | } 156 | 157 | @Override 158 | protected void process(List chunks) { 159 | super.process(chunks); 160 | } 161 | 162 | @Override 163 | protected void done() { 164 | try { 165 | treeView.setRootWithFile(get(), new File(path).getName()); 166 | } catch (Exception e) { 167 | e.printStackTrace(); 168 | new ErrorDialog(MainWindow.this, e); 169 | } 170 | } 171 | }.execute(); 172 | 173 | } 174 | 175 | private void selectPathToSave() { 176 | JFileChooser fileChooser = new JFileChooser(); 177 | fileChooser.setAcceptAllFileFilterUsed(false); 178 | FileNameExtensionFilter filter = new FileNameExtensionFilter("Android resource files", "arsc"); 179 | fileChooser.addChoosableFileFilter(filter); 180 | String fileName = Files.getNameWithoutExtension(openedFilePath); 181 | 182 | fileChooser.setSelectedFile(new File(new File(openedFilePath).getParent() + "/" 183 | + fileName + "_mod.arsc")); 184 | int result = fileChooser.showSaveDialog(getRootPane()); 185 | 186 | if (result == JFileChooser.APPROVE_OPTION) { 187 | saveFile(fileChooser.getSelectedFile()); 188 | } 189 | } 190 | 191 | private void saveFile(File path) { 192 | new SwingWorker() { 193 | @Override 194 | protected Boolean doInBackground() throws Exception { 195 | new ArscWriter(ValueHelper.getResourceTableChunk(), path).write(); 196 | return true; 197 | } 198 | 199 | @Override 200 | protected void done() { 201 | try { 202 | saveAs.setEnabled(false); 203 | JOptionPane.showMessageDialog(MainWindow.this, "File saved!"); 204 | } catch (Exception e) { 205 | e.printStackTrace(); 206 | new ErrorDialog(MainWindow.this, e); 207 | } 208 | } 209 | }.execute(); 210 | 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /arsceditor/src/main/java/com/mrikso/arsceditor/gui/dialogs/ResourceEditDialog.java: -------------------------------------------------------------------------------- 1 | package com.mrikso.arsceditor.gui.dialogs; 2 | 3 | import com.mrikso.arsceditor.gui.tree.JTableTreeNode; 4 | import com.mrikso.arsceditor.valueeditor.ValueType; 5 | 6 | import javax.swing.*; 7 | import java.awt.*; 8 | import java.awt.event.ItemEvent; 9 | import java.awt.event.ItemListener; 10 | 11 | public class ResourceEditDialog extends JDialog implements ItemListener { 12 | 13 | private ValueType valueType; 14 | private final String resId; 15 | private final String resName; 16 | private final String resValue; 17 | private final String resDecodedName; 18 | private final String resDecodeValue; 19 | 20 | private JTextField nameTextField; 21 | private JTextArea valueArea; 22 | private ValueChangedListener valueChangedListener; 23 | private boolean isComplex; 24 | private boolean isChildren; 25 | 26 | public ResourceEditDialog(JFrame parent, JTableTreeNode defaultMutableTreeNode) { 27 | super(parent); 28 | 29 | valueType = defaultMutableTreeNode.getValueType(); 30 | resId = defaultMutableTreeNode.getId(); 31 | resName = defaultMutableTreeNode.getName(); 32 | resValue = defaultMutableTreeNode.getValue(); 33 | resDecodedName = defaultMutableTreeNode.getDecodedName(); 34 | resDecodeValue = defaultMutableTreeNode.getDecodeValue(); 35 | isComplex = defaultMutableTreeNode.isComplex(); 36 | isChildren = defaultMutableTreeNode.isChildren(); 37 | 38 | initUI(); 39 | pack(); 40 | setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE); 41 | //setSize(new Dimension(500, 300)); 42 | setResizable(false); 43 | setTitle("Edit value"); 44 | setModal(true); 45 | setLocationRelativeTo(parent); 46 | } 47 | 48 | private void initUI() { 49 | 50 | // CompactGrid layout is from http://sourceforge.net/projects/swinglib/ 51 | JPanel pFields = new JPanel(new CompactGridLayout(2, 6, 12)); 52 | JTextField idTextFiled = new JTextField(30); 53 | int defaultWidth = (int) idTextFiled.getPreferredSize().getWidth(); 54 | 55 | if (resId != null) { 56 | idTextFiled.setText(resId); 57 | idTextFiled.setEditable(false); 58 | pFields.add(new JLabel("ID:")); 59 | pFields.add(idTextFiled); 60 | } 61 | 62 | nameTextField = new JTextField(); 63 | nameTextField.setPreferredSize(new Dimension(defaultWidth, nameTextField.getPreferredSize().height)); 64 | nameTextField.setText(resName); 65 | 66 | pFields.add(new JLabel("Name:")); 67 | pFields.add(nameTextField); 68 | 69 | if(resDecodedName != null) { 70 | JTextField decodedNameTextField = new JTextField(); 71 | decodedNameTextField.setPreferredSize(new Dimension(defaultWidth, nameTextField.getPreferredSize().height)); 72 | decodedNameTextField.setText(resDecodedName); 73 | decodedNameTextField.setEditable(false); 74 | 75 | pFields.add(new JLabel("Decoded Name:")); 76 | pFields.add(decodedNameTextField); 77 | } 78 | 79 | if (valueType != null) { 80 | String[] smaliSearchTypes = {"String", "Reference", 81 | "Attribute", "Color", "Int-dec", "Int-hex", "Boolean", "Float", "Dimension", "Fraction", "DynamicReference"}; 82 | JComboBox comboBoxTypes = new JComboBox<>(smaliSearchTypes); 83 | comboBoxTypes.setSelectedIndex(valueType.getValue()); 84 | comboBoxTypes.setEditable(false); 85 | comboBoxTypes.addItemListener(this); 86 | comboBoxTypes.setPreferredSize(new Dimension(defaultWidth, comboBoxTypes.getPreferredSize().height)); 87 | pFields.add(new JLabel("Type:")); 88 | pFields.add(comboBoxTypes); 89 | 90 | if(resDecodeValue != null) { 91 | JTextField decodedValueTextFiled = new JTextField(); 92 | decodedValueTextFiled.setPreferredSize(new Dimension(defaultWidth, nameTextField.getPreferredSize().height)); 93 | decodedValueTextFiled.setText(resDecodeValue); 94 | decodedValueTextFiled.setEditable(false); 95 | 96 | pFields.add(new JLabel("Decoded Value:")); 97 | pFields.add(decodedValueTextFiled); 98 | } 99 | 100 | JScrollPane scrollPane; 101 | if (valueType == ValueType.TYPE_STRING) { 102 | valueArea = new JTextArea(resValue, 10, 27); 103 | valueArea.setLineWrap(true); 104 | valueArea.setWrapStyleWord(true); 105 | valueArea.setFont(idTextFiled.getFont()); 106 | } else { 107 | valueArea = new JTextArea(resValue, 1, 27); 108 | valueArea.setLineWrap(true); 109 | valueArea.setWrapStyleWord(true); 110 | valueArea.setFont(idTextFiled.getFont()); 111 | valueArea.getDocument().putProperty("filterNewLines", Boolean.TRUE); 112 | } 113 | scrollPane = new JScrollPane(valueArea); 114 | 115 | pFields.add(new JLabel("Value:")); 116 | pFields.add(scrollPane); 117 | } 118 | 119 | pFields.setBorder(BorderFactory.createEmptyBorder(12, 12, 12, 12)); 120 | 121 | JButton okButton = new JButton("OK"); 122 | okButton.addActionListener(l -> { 123 | save(); 124 | }); 125 | JButton cancelButton = new JButton("Cancel"); 126 | cancelButton.addActionListener(l -> { 127 | setVisible(false); 128 | }); 129 | 130 | JPanel pButtons = new JPanel(new FlowLayout(FlowLayout.CENTER)); 131 | pButtons.add(okButton); 132 | pButtons.add(cancelButton); 133 | 134 | JPanel pContent = new JPanel(new BorderLayout()); 135 | pContent.add(pFields, BorderLayout.CENTER); 136 | pContent.add(pButtons, BorderLayout.SOUTH); 137 | 138 | setContentPane(pContent); 139 | } 140 | 141 | public void setValueChangedListener(ValueChangedListener valueChangedListener) { 142 | this.valueChangedListener = valueChangedListener; 143 | } 144 | 145 | private void save() { 146 | if (valueChangedListener != null) { 147 | String resName = nameTextField.getText(); 148 | if(isComplex){ 149 | valueChangedListener.onResourceEdited(resName, resId, null, null, false); 150 | }else { 151 | String resValue = valueArea.getText(); 152 | valueChangedListener.onResourceEdited(resName, resId, resValue, valueType, isChildren); 153 | } 154 | } 155 | setVisible(false); 156 | } 157 | 158 | public void showDialog() { 159 | setVisible(true); 160 | } 161 | 162 | @Override 163 | public void itemStateChanged(ItemEvent e) { 164 | if (e.getStateChange() == ItemEvent.SELECTED) { 165 | switch (e.getItem().toString()) { 166 | case "String": 167 | valueType = ValueType.TYPE_STRING; 168 | break; 169 | case "Reference": 170 | valueType = ValueType.TYPE_REFERENCE; 171 | break; 172 | case "Attribute": 173 | valueType = ValueType.TYPE_ATTRIBUTE; 174 | break; 175 | case "Color": 176 | valueType = ValueType.TYPE_COLOR; 177 | break; 178 | case "Int-dec": 179 | valueType = ValueType.TYPE_INT_DEC; 180 | break; 181 | case "Int-hex": 182 | valueType = ValueType.TYPE_INT_HEX; 183 | break; 184 | case "Boolean": 185 | valueType = ValueType.TYPE_BOOLEAN; 186 | break; 187 | case "Float": 188 | valueType = ValueType.TYPE_FLOAT; 189 | break; 190 | case "Dimension": 191 | valueType = ValueType.TYPE_DIMENSION; 192 | break; 193 | case "Fraction": 194 | valueType = ValueType.TYPE_FRACTION; 195 | break; 196 | case "DynamicReference": 197 | valueType = ValueType.TYPE_DYNAMIC_REFERENCE; 198 | break; 199 | } 200 | } 201 | } 202 | 203 | public interface ValueChangedListener { 204 | public void onResourceEdited(String name, String id, String value, ValueType resourceType, boolean isChildren); 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /arsceditor/src/main/java/com/mrikso/arsceditor/gui/treetable/AbstractTreeTableModel.java: -------------------------------------------------------------------------------- 1 | package com.mrikso.arsceditor.gui.treetable; 2 | 3 | import javax.swing.event.EventListenerList; 4 | import javax.swing.event.TreeModelEvent; 5 | import javax.swing.event.TreeModelListener; 6 | import javax.swing.table.AbstractTableModel; 7 | import javax.swing.tree.TreePath; 8 | 9 | /** 10 | * An abstract implementation of the TreeTableModel interface, handling the list 11 | * of listeners. 12 | * 13 | * @see AbstractTableModel 14 | */ 15 | public abstract class AbstractTreeTableModel implements TreeTableModel { 16 | 17 | /** The root. */ 18 | protected Object root; 19 | 20 | /** The listener list. */ 21 | protected EventListenerList listenerList = new EventListenerList(); 22 | 23 | /** 24 | * Instantiates a new abstract tree table model. 25 | * 26 | * @param root 27 | * the root 28 | */ 29 | public AbstractTreeTableModel(Object root) { 30 | this.root = root; 31 | } 32 | 33 | // 34 | // Default implementations for methods in the TreeModel interface. 35 | // 36 | 37 | @Override 38 | public Object getRoot() { 39 | return root; 40 | } 41 | 42 | @Override 43 | public boolean isLeaf(Object node) { 44 | return getChildCount(node) == 0; 45 | } 46 | 47 | @Override 48 | public void valueForPathChanged(TreePath path, Object newValue) { 49 | } 50 | 51 | // This is not called in the JTree's default mode: 52 | // use a naive implementation. 53 | @Override 54 | public int getIndexOfChild(Object parent, Object child) { 55 | for (int i = 0; i < getChildCount(parent); i++) { 56 | if (getChild(parent, i).equals(child)) { 57 | return i; 58 | } 59 | } 60 | return -1; 61 | } 62 | 63 | @Override 64 | public void addTreeModelListener(TreeModelListener l) { 65 | listenerList.add(TreeModelListener.class, l); 66 | } 67 | 68 | @Override 69 | public void removeTreeModelListener(TreeModelListener l) { 70 | listenerList.remove(TreeModelListener.class, l); 71 | } 72 | 73 | /* 74 | * Notifies all listeners that have registered interest for notification on 75 | * this event type. The event instance is lazily created using the 76 | * parameters passed into the fire method. 77 | * 78 | * @see EventListenerList 79 | */ 80 | protected void fireTreeNodesChanged(Object source, Object[] path, 81 | int[] childIndices, Object[] children) { 82 | // Guaranteed to return a non-null array 83 | Object[] listeners = listenerList.getListenerList(); 84 | TreeModelEvent e = null; 85 | // Process the listeners last to first, notifying 86 | // those that are interested in this event 87 | for (int i = listeners.length - 2; i >= 0; i -= 2) { 88 | if (listeners[i] == TreeModelListener.class) { 89 | // Lazily create the event: 90 | if (e == null) 91 | e = new TreeModelEvent(source, path, childIndices, children); 92 | ((TreeModelListener) listeners[i + 1]).treeNodesChanged(e); 93 | } 94 | } 95 | } 96 | 97 | /* 98 | * Notifies all listeners that have registered interest for notification on 99 | * this event type. The event instance is lazily created using the 100 | * parameters passed into the fire method. 101 | * 102 | * @see EventListenerList 103 | */ 104 | protected void fireTreeNodesInserted(Object source, Object[] path, 105 | int[] childIndices, Object[] children) { 106 | // Guaranteed to return a non-null array 107 | Object[] listeners = listenerList.getListenerList(); 108 | TreeModelEvent e = null; 109 | // Process the listeners last to first, notifying 110 | // those that are interested in this event 111 | for (int i = listeners.length - 2; i >= 0; i -= 2) { 112 | if (listeners[i] == TreeModelListener.class) { 113 | // Lazily create the event: 114 | if (e == null) 115 | e = new TreeModelEvent(source, path, childIndices, children); 116 | ((TreeModelListener) listeners[i + 1]).treeNodesInserted(e); 117 | } 118 | } 119 | } 120 | 121 | /* 122 | * Notifies all listeners that have registered interest for notification on 123 | * this event type. The event instance is lazily created using the 124 | * parameters passed into the fire method. 125 | * 126 | * @see EventListenerList 127 | */ 128 | protected void fireTreeNodesRemoved(Object source, Object[] path, 129 | int[] childIndices, Object[] children) { 130 | // Guaranteed to return a non-null array 131 | Object[] listeners = listenerList.getListenerList(); 132 | TreeModelEvent e = null; 133 | // Process the listeners last to first, notifying 134 | // those that are interested in this event 135 | for (int i = listeners.length - 2; i >= 0; i -= 2) { 136 | if (listeners[i] == TreeModelListener.class) { 137 | // Lazily create the event: 138 | if (e == null) 139 | e = new TreeModelEvent(source, path, childIndices, children); 140 | ((TreeModelListener) listeners[i + 1]).treeNodesRemoved(e); 141 | } 142 | } 143 | } 144 | 145 | /* 146 | * Notifies all listeners that have registered interest for notification on 147 | * this event type. The event instance is lazily created using the 148 | * parameters passed into the fire method. 149 | * 150 | * @see EventListenerList 151 | */ 152 | protected void fireTreeStructureChanged(Object source, Object[] path, 153 | int[] childIndices, Object[] children) { 154 | // Guaranteed to return a non-null array 155 | Object[] listeners = listenerList.getListenerList(); 156 | TreeModelEvent e = null; 157 | // Process the listeners last to first, notifying 158 | // those that are interested in this event 159 | for (int i = listeners.length - 2; i >= 0; i -= 2) { 160 | if (listeners[i] == TreeModelListener.class) { 161 | // Lazily create the event: 162 | if (e == null) 163 | e = new TreeModelEvent(source, path, null, children); 164 | ((TreeModelListener) listeners[i + 1]).treeStructureChanged(e); 165 | } 166 | } 167 | } 168 | 169 | // 170 | // Default impelmentations for methods in the TreeTableModel interface. 171 | // 172 | 173 | @Override 174 | public Class getColumnClass(int column) { 175 | if (column == 0) { 176 | return TreeTableModel.class; 177 | } 178 | return Object.class; 179 | } 180 | 181 | /** 182 | * By default, make the column with the Tree in it the only editable one. 183 | * Making this column editable causes the JTable to forward mouse and 184 | * keyboard events in the Tree column to the underlying JTree. 185 | */ 186 | @Override 187 | public boolean isCellEditable(Object node, int column) { 188 | return getColumnClass(column) == TreeTableModel.class; 189 | } 190 | 191 | @Override 192 | public void setValueAt(Object aValue, Object node, int column) { 193 | } 194 | 195 | /** 196 | * Returns a column given its name. Implementation is naive so this should 197 | * be overridden if this method is to be called often. This method is not in 198 | * the TableModel interface and is not used by the 199 | * JTable. 200 | * 201 | * @param columnName 202 | * string containing name of column to be located 203 | * @return the column with columnName, or -1 if not found 204 | */ 205 | public int findColumn(String columnName) { 206 | for (int i = 0; i < getColumnCount(); i++) { 207 | if (columnName.equals(getColumnName(i))) { 208 | return i; 209 | } 210 | } 211 | return -1; 212 | } 213 | 214 | /** 215 | * Returns a default name for the column using spreadsheet conventions: A, 216 | * B, C, ... Z, AA, AB, etc. If column cannot be found, returns 217 | * an empty string. 218 | * 219 | * @param column 220 | * the column being queried 221 | * @return a string containing the default name of column 222 | */ 223 | @Override 224 | public String getColumnName(int column) { 225 | String result = ""; 226 | for (; column >= 0; column = column / 26 - 1) { 227 | result = (char) ((char) (column % 26) + 'A') + result; 228 | } 229 | return result; 230 | } 231 | } 232 | -------------------------------------------------------------------------------- /arsceditor/src/main/java/com/mrikso/arsceditor/gui/treetable/DynamicTreeTableModel.java: -------------------------------------------------------------------------------- 1 | package com.mrikso.arsceditor.gui.treetable; 2 | 3 | import java.lang.reflect.Constructor; 4 | import java.lang.reflect.Method; 5 | 6 | import javax.swing.event.TableModelListener; 7 | import javax.swing.tree.TreeNode; 8 | 9 | /** 10 | * 动态生成表的项 11 | */ 12 | public class DynamicTreeTableModel extends AbstractTreeTableModel { 13 | 14 | /** Names of the columns, used for the TableModel getColumnName method. */ 15 | private final String[] columnNames; 16 | /** 17 | * Method names used to determine a particular value. Used for the 18 | * TableModel method getValueAt. 19 | */ 20 | private final String[] methodNames; 21 | /** 22 | * Setter method names, used to set a particular value. Used for the 23 | * TableModel method setValueAt. A null entry, or array, indicates the 24 | * column is not editable. 25 | */ 26 | private final String[] setterMethodNames; 27 | /** Column classes, used for the TableModel method getColumnClass. */ 28 | private final Class[] cTypes; 29 | 30 | // //////////////////////////////////////////////////////////////////////// 31 | // TODO 32 | // 考虑到易用性,最好是动态生成类的set和get方法,以后补充 33 | // TODO 34 | // //////////////////////////////////////////////////////////////////////// 35 | 36 | /** 37 | * Constructor for creating a DynamicTreeTableModel. 38 | */ 39 | public DynamicTreeTableModel(TreeNode root, String[] columnNames, 40 | String[] getterMethodNames, String[] setterMethodNames, 41 | Class[] cTypes) { 42 | super(root); 43 | this.columnNames = columnNames; 44 | this.methodNames = getterMethodNames; 45 | this.setterMethodNames = setterMethodNames; 46 | this.cTypes = cTypes; 47 | } 48 | 49 | // 50 | // TreeModel interface 51 | // 52 | 53 | /** 54 | * TreeModel method to return the number of children of a particular node. 55 | * Since node is a TreeNode, this can be answered via the 56 | * TreeNode method getChildCount. 57 | */ 58 | @Override 59 | public int getChildCount(Object node) { 60 | return ((TreeNode) node).getChildCount(); 61 | } 62 | 63 | /** 64 | * TreeModel method to locate a particular child of the specified node. 65 | * Since node is a TreeNode, this can be answered via the 66 | * TreeNode method getChild. 67 | */ 68 | @Override 69 | public Object getChild(Object node, int i) { 70 | return ((TreeNode) node).getChildAt(i); 71 | } 72 | 73 | /** 74 | * TreeModel method to determine if a node is a leaf. Since 75 | * node is a TreeNode, this can be answered via the TreeNode 76 | * method isLeaf. 77 | */ 78 | @Override 79 | public boolean isLeaf(Object node) { 80 | return ((TreeNode) node).isLeaf(); 81 | } 82 | 83 | // 84 | // The TreeTable interface. 85 | // 86 | 87 | /** 88 | * Returns the number of column names passed into the constructor. 89 | */ 90 | @Override 91 | public int getColumnCount() { 92 | return columnNames.length; 93 | } 94 | 95 | /** 96 | * Returns the column name passed into the constructor. 97 | */ 98 | @Override 99 | public String getColumnName(int column) { 100 | if (columnNames == null || column < 0 || column >= columnNames.length) { 101 | return null; 102 | } 103 | return columnNames[column]; 104 | } 105 | 106 | /** 107 | * Returns the column class for column column. This is set in 108 | * the constructor. 109 | */ 110 | @Override 111 | public Class getColumnClass(int column) { 112 | if (cTypes == null || column < 0 || column >= cTypes.length) { 113 | return null; 114 | } 115 | return cTypes[column]; 116 | } 117 | 118 | /** 119 | * Returns the value for the column column and object 120 | * node. The return value is determined by invoking the method 121 | * specified in constructor for the passed in column. 122 | */ 123 | @Override 124 | public Object getValueAt(Object node, int column) { 125 | try { 126 | Method method = node.getClass().getMethod(methodNames[column]); 127 | return method.invoke(node); 128 | } catch (Throwable th) { 129 | } 130 | 131 | return null; 132 | } 133 | 134 | /** 135 | * Returns true if there is a setter method name for column 136 | * column. This is set in the constructor. 137 | */ 138 | @Override 139 | public boolean isCellEditable(Object node, int column) { 140 | return false; 141 | } 142 | 143 | /** 144 | * Sets the value to aValue for the object node in 145 | * column column. This is done by using the setter method name, 146 | * and coercing the passed in value to the specified type. 147 | */ 148 | // Note: This looks up the methods each time! This is rather inefficient; 149 | // it should really be changed to cache matching methods/constructors 150 | // based on node's class, and aValue's class. 151 | @Override 152 | public void setValueAt(Object aValue, Object node, int column) { 153 | boolean found = false; 154 | try { 155 | // We have to search through all the methods since the 156 | // types may not match up. 157 | Method[] methods = node.getClass().getMethods(); 158 | 159 | for (int counter = methods.length - 1; counter >= 0; counter--) { 160 | if (methods[counter].getName() 161 | .equals(setterMethodNames[column]) 162 | && methods[counter].getParameterTypes() != null 163 | && methods[counter].getParameterTypes().length == 1) { 164 | // We found a matching method 165 | Class param = methods[counter].getParameterTypes()[0]; 166 | if (!param.isInstance(aValue)) { 167 | // Yes, we can use the value passed in directly, 168 | // no coercision is necessary! 169 | if (aValue instanceof String 170 | && ((String) aValue).length() == 0) { 171 | // Assume an empty string is null, this is 172 | // probably bogus for here. 173 | aValue = null; 174 | } else { 175 | // Have to attempt some sort of coercision. 176 | // See if the expected parameter type has 177 | // a constructor that takes a String. 178 | Constructor cs = param 179 | .getConstructor(String.class); 180 | if (cs != null) { 181 | aValue = cs 182 | .newInstance(aValue); 183 | } else { 184 | aValue = null; 185 | } 186 | } 187 | } 188 | // null either means it was an empty string, or there 189 | // was no translation. Could potentially deal with these 190 | // differently. 191 | methods[counter].invoke(node, aValue); 192 | found = true; 193 | break; 194 | } 195 | } 196 | } catch (Throwable th) { 197 | System.out.println("exception: " + th); 198 | } 199 | if (found) { 200 | // The value changed, fire an event to notify listeners. 201 | TreeNode parent = ((TreeNode) node).getParent(); 202 | fireTreeNodesChanged(this, getPathToRoot(parent), 203 | new int[] { getIndexOfChild(parent, node) }, 204 | new Object[] { node }); 205 | } 206 | } 207 | 208 | /** 209 | * Builds the parents of the node up to and including the root node, where 210 | * the original node is the last element in the returned array. The length 211 | * of the returned array gives the node's depth in the tree. 212 | * 213 | * @param aNode 214 | * the TreeNode to get the path for 215 | * @param an 216 | * array of TreeNodes giving the path from the root to the 217 | * specified node. 218 | */ 219 | public TreeNode[] getPathToRoot(TreeNode aNode) { 220 | return getPathToRoot(aNode, 0); 221 | } 222 | 223 | /** 224 | * Builds the parents of the node up to and including the root node, where 225 | * the original node is the last element in the returned array. The length 226 | * of the returned array gives the node's depth in the tree. 227 | * 228 | * @param aNode 229 | * the TreeNode to get the path for 230 | * @param depth 231 | * an int giving the number of steps already taken towards the 232 | * root (on recursive calls), used to size the returned array 233 | * @return an array of TreeNodes giving the path from the root to the 234 | * specified node 235 | */ 236 | private TreeNode[] getPathToRoot(TreeNode aNode, int depth) { 237 | TreeNode[] retNodes; 238 | // This method recurses, traversing towards the root in order 239 | // size the array. On the way back, it fills in the nodes, 240 | // starting from the root and working back to the original node. 241 | 242 | /* 243 | * Check for null, in case someone passed in a null node, or they passed 244 | * in an element that isn't rooted at root. 245 | */ 246 | if (aNode == null) { 247 | if (depth == 0) 248 | return null; 249 | else 250 | retNodes = new TreeNode[depth]; 251 | } else { 252 | depth++; 253 | if (aNode == root) 254 | retNodes = new TreeNode[depth]; 255 | else 256 | retNodes = getPathToRoot(aNode.getParent(), depth); 257 | retNodes[retNodes.length - depth] = aNode; 258 | } 259 | return retNodes; 260 | } 261 | 262 | @Override 263 | public void addTableModelListener(TableModelListener l) { 264 | } 265 | 266 | @Override 267 | public void removeTableModelListener(TableModelListener l) { 268 | } 269 | } 270 | -------------------------------------------------------------------------------- /arsceditor/src/main/java/com/mrikso/arsceditor/util/ResourceHelper2.java: -------------------------------------------------------------------------------- 1 | package com.mrikso.arsceditor.util; 2 | 3 | /* 4 | * Copyright (C) 2008 The Android Open Source Project 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | 19 | 20 | import java.util.regex.Matcher; 21 | import java.util.regex.Pattern; 22 | 23 | /** 24 | * Helper class to provide various conversion method used in handling android resources. 25 | */ 26 | public final class ResourceHelper2 { 27 | 28 | private final static Pattern sFloatPattern = Pattern.compile("(-?[0-9]+(?:\\.[0-9]+)?)(.*)"); 29 | private final static float[] sFloatOut = new float[1]; 30 | 31 | private final static TypedValue mValue = new TypedValue(); 32 | 33 | // ------- TypedValue stuff 34 | // This is taken from //device/libs/utils/ResourceTypes.cpp 35 | 36 | private static final class UnitEntry { 37 | String name; 38 | int type; 39 | int unit; 40 | float scale; 41 | 42 | UnitEntry(String name, int type, int unit, float scale) { 43 | this.name = name; 44 | this.type = type; 45 | this.unit = unit; 46 | this.scale = scale; 47 | } 48 | } 49 | 50 | private final static UnitEntry[] sUnitNames = new UnitEntry[] { 51 | new UnitEntry("px", TypedValue.TYPE_DIMENSION, TypedValue.COMPLEX_UNIT_PX, 1.0f), 52 | new UnitEntry("dip", TypedValue.TYPE_DIMENSION, TypedValue.COMPLEX_UNIT_DIP, 1.0f), 53 | new UnitEntry("dp", TypedValue.TYPE_DIMENSION, TypedValue.COMPLEX_UNIT_DIP, 1.0f), 54 | new UnitEntry("sp", TypedValue.TYPE_DIMENSION, TypedValue.COMPLEX_UNIT_SP, 1.0f), 55 | new UnitEntry("pt", TypedValue.TYPE_DIMENSION, TypedValue.COMPLEX_UNIT_PT, 1.0f), 56 | new UnitEntry("in", TypedValue.TYPE_DIMENSION, TypedValue.COMPLEX_UNIT_IN, 1.0f), 57 | new UnitEntry("mm", TypedValue.TYPE_DIMENSION, TypedValue.COMPLEX_UNIT_MM, 1.0f), 58 | new UnitEntry("%", TypedValue.TYPE_FRACTION, TypedValue.COMPLEX_UNIT_FRACTION, 1.0f/100), 59 | new UnitEntry("%p", TypedValue.TYPE_FRACTION, TypedValue.COMPLEX_UNIT_FRACTION_PARENT, 1.0f/100), 60 | }; 61 | 62 | /** 63 | * Returns the raw value from the given attribute float-type value string. 64 | * This object is only valid until the next call on to {@link ResourceHelper2}. 65 | * 66 | * @param attribute Attribute name. 67 | * @param value Attribute value. 68 | * @param requireUnit whether the value is expected to contain a unit. 69 | * @return The typed value. 70 | */ 71 | public static TypedValue getValue(String attribute, String value, boolean requireUnit) { 72 | if (parseFloatAttribute(attribute, value, mValue, requireUnit)) { 73 | return mValue; 74 | } 75 | 76 | return null; 77 | } 78 | 79 | /** 80 | * Parse a float attribute and return the parsed value into a given TypedValue. 81 | * @param attribute the name of the attribute. Can be null if requireUnit is false. 82 | * @param value the string value of the attribute 83 | * @param outValue the TypedValue to receive the parsed value 84 | * @param requireUnit whether the value is expected to contain a unit. 85 | * @return true if success. 86 | */ 87 | public static boolean parseFloatAttribute(String attribute, String value, 88 | TypedValue outValue, boolean requireUnit) { 89 | // assert requireUnit == false || attribute != null; 90 | 91 | // remove the space before and after 92 | value = value.trim(); 93 | int len = value.length(); 94 | 95 | if (len <= 0) { 96 | return false; 97 | } 98 | 99 | // check that there's no non ascii characters. 100 | char[] buf = value.toCharArray(); 101 | for (int i = 0 ; i < len ; i++) { 102 | if (buf[i] > 255) { 103 | return false; 104 | } 105 | } 106 | 107 | // check the first character 108 | if (buf[0] < '0' && buf[0] > '9' && buf[0] != '.' && buf[0] != '-') { 109 | return false; 110 | } 111 | 112 | // now look for the string that is after the float... 113 | Matcher m = sFloatPattern.matcher(value); 114 | if (m.matches()) { 115 | String f_str = m.group(1); 116 | String end = m.group(2); 117 | 118 | float f; 119 | try { 120 | f = Float.parseFloat(f_str); 121 | } catch (NumberFormatException e) { 122 | // this shouldn't happen with the regexp above. 123 | return false; 124 | } 125 | 126 | if (end.length() > 0 && end.charAt(0) != ' ') { 127 | // Might be a unit... 128 | if (parseUnit(end, outValue, sFloatOut)) { 129 | computeTypedValue(outValue, f, sFloatOut[0], end); 130 | return true; 131 | } 132 | return false; 133 | } 134 | 135 | // make sure it's only spaces at the end. 136 | end = end.trim(); 137 | 138 | if (end.length() == 0) { 139 | if (outValue != null) { 140 | outValue.assetCookie = 0; 141 | outValue.string = null; 142 | 143 | if (requireUnit == false) { 144 | outValue.type = TypedValue.TYPE_FLOAT; 145 | outValue.data = Float.floatToIntBits(f); 146 | } else { 147 | // no unit when required? Use dp and out an error. 148 | applyUnit(sUnitNames[1], outValue, sFloatOut); 149 | computeTypedValue(outValue, f, sFloatOut[0], "dp"); 150 | 151 | System.out.println(String.format( 152 | "Dimension \"%1$s\" in attribute \"%2$s\" is missing unit!", 153 | value, attribute == null ? "(unknown)" : attribute)); 154 | } 155 | return true; 156 | } 157 | } 158 | } 159 | 160 | return false; 161 | } 162 | 163 | private static void computeTypedValue(TypedValue outValue, float value, float scale, String unit) { 164 | value *= scale; 165 | boolean neg = value < 0; 166 | if (neg) { 167 | value = -value; 168 | } 169 | long bits = (long)(value*(1<<23)+.5f); 170 | int radix; 171 | int shift; 172 | if ((bits&0x7fffff) == 0) { 173 | // Always use 23p0 if there is no fraction, just to make 174 | // things easier to read. 175 | radix = TypedValue.COMPLEX_RADIX_23p0; 176 | shift = 23; 177 | } else if ((bits&0xffffffffff800000L) == 0) { 178 | // Magnitude is zero -- can fit in 0 bits of precision. 179 | radix = TypedValue.COMPLEX_RADIX_0p23; 180 | shift = 0; 181 | } else if ((bits&0xffffffff80000000L) == 0) { 182 | // Magnitude can fit in 8 bits of precision. 183 | radix = TypedValue.COMPLEX_RADIX_8p15; 184 | shift = 8; 185 | } else if ((bits&0xffffff8000000000L) == 0) { 186 | // Magnitude can fit in 16 bits of precision. 187 | radix = TypedValue.COMPLEX_RADIX_16p7; 188 | shift = 16; 189 | } else { 190 | // Magnitude needs entire range, so no fractional part. 191 | radix = TypedValue.COMPLEX_RADIX_23p0; 192 | shift = 23; 193 | } 194 | int mantissa = (int)( 195 | (bits>>shift) & TypedValue.COMPLEX_MANTISSA_MASK); 196 | if (neg) { 197 | mantissa = (-mantissa) & TypedValue.COMPLEX_MANTISSA_MASK; 198 | } 199 | outValue.data |= (radix< chunks, String fileName) { 51 | try { 52 | ArscNode root = new ArscNode(fileName); 53 | arscReader(chunks, root); 54 | setRoot(root); 55 | } catch (Exception e) { 56 | e.printStackTrace(); 57 | } 58 | } 59 | 60 | private void arscReader(List chunks, ArscNode root) throws IOException { 61 | if (chunks.isEmpty()) { 62 | throw new IOException("no chunks"); 63 | } 64 | 65 | if (!(chunks.get(0) instanceof ResourceTableChunk)) { 66 | throw new IOException("no res table chunk"); 67 | } 68 | 69 | AttrNameHelper attrNameHelper = AttrNameHelper.getInstance(); 70 | HashMap attrId = new HashMap<>(); 71 | 72 | for (Chunk chunk : chunks) { 73 | ResourceTableChunk resourceTableChunk = (ResourceTableChunk) chunk; 74 | ValueHelper.setResourceTableChunk(resourceTableChunk); 75 | ValueHelper.setStringPoolChunk(resourceTableChunk.getStringPool()); 76 | 77 | // getting all packages 78 | for (PackageChunk packageChunk : resourceTableChunk.getPackages()) { 79 | String packageName = packageChunk.getPackageName(); 80 | ArscNode resTableNode = new ArscNode(String.format("%s (%s)", packageName, packageChunk.getId()), packageName); 81 | resTableNode.setId(packageChunk.getId()); 82 | resTableNode.setRootPackage(true); 83 | resTableNode.setPackageChunk(packageChunk); 84 | // getting all types 85 | for (TypeSpecChunk typeSpecChunk : packageChunk.getTypeSpecChunks()) { 86 | String typeName = typeSpecChunk.getTypeName(); 87 | ArscNode typeNode = new ArscNode(typeName); 88 | 89 | int resTypeId = typeSpecChunk.getId(); 90 | 91 | // getting all configs from types 92 | for (TypeChunk typeChunk : packageChunk.getTypeChunks(typeSpecChunk.getId())) { 93 | String config = typeChunk.getConfiguration().toString(); 94 | ArscNode configNode; 95 | if (config.equals("default")) { 96 | configNode = new ArscNode(config); 97 | } else { 98 | configNode = new ArscNode(String.format("%s-%s", typeName, config)); 99 | } 100 | //configNode.setStringPool(resourceTableChunk.getStringPool()); 101 | configNode.setPackageChunk(packageChunk); 102 | configNode.setTypeSpec(typeSpecChunk); 103 | configNode.setType(typeChunk); 104 | typeNode.add(configNode); 105 | 106 | // getting all ids on this package 107 | for (Map.Entry entry : typeChunk.getEntries().entrySet()) { 108 | int entryId = entry.getKey(); 109 | int resId = getResId(packageChunk.getId(), resTypeId, entryId); 110 | // System.out.println(String.format("%d = %s, %s", resId, entry.getValue().key(), entry.getValue().typeName())); 111 | attrId.put(resId, new ResId(resId, entry.getValue().key(), entry.getValue().typeName())); 112 | } 113 | } 114 | if (typeNode.children().hasMoreElements()) { 115 | resTableNode.add(typeNode); 116 | } 117 | } 118 | 119 | root.add(resTableNode); 120 | } 121 | 122 | } 123 | 124 | attrNameHelper.setAttrPackageMap(attrId); 125 | } 126 | 127 | private static int getResId(int packId, int resTypeId, int entryId) { 128 | return (((packId) << 24) | (((resTypeId) & 0xFF) << 16) | (entryId & 0xFFFF)); 129 | } 130 | 131 | private void setRoot(DefaultMutableTreeNode root) { 132 | arscTableView.setModel(new DefaultTableModel()); 133 | 134 | DefaultTreeModel treeModel = (DefaultTreeModel) getModel(); 135 | treeModel.setRoot(root); 136 | } 137 | 138 | public void setTableChangedListener(TableChangedListener tableChangedListener) { 139 | this.tableChangedListener = tableChangedListener; 140 | } 141 | 142 | @Nullable 143 | public ArscNode getSelectedNode() { 144 | TreePath path = getSelectionPath(); 145 | assert path != null; 146 | Object component = path.getLastPathComponent(); 147 | return component instanceof ArscNode ? (ArscNode) component : null; 148 | } 149 | 150 | private void openNode() { 151 | final ArscNode node = Objects.requireNonNull(getSelectedNode()); 152 | 153 | if (!node.children().hasMoreElements()) { 154 | arscTableView.setTreeTableModel(createModel(node)); 155 | arscTableView.updateTable(); 156 | } 157 | } 158 | 159 | protected ResourceTypeTableModel createModel(ArscNode node) { 160 | ResourceModel resourceModel = new ResourceModel(ValueHelper.getStringPoolChunk(), node.getPackageChunk(), 161 | node.getTypeSpec(), node.getTypeChunk(), node.getName()); 162 | resourceModel.readTable(); 163 | return new ResourceTypeTableModel(resourceModel.getRoot()); 164 | } 165 | 166 | @Override 167 | public void mouseClicked(MouseEvent e) { 168 | int row = getRowForLocation(e.getX(), e.getY()); 169 | if (row >= 0) { 170 | if (SwingUtilities.isLeftMouseButton(e) && e.getClickCount() == 2) { 171 | Thread someThread = new Thread(this::openNode); 172 | someThread.start(); 173 | 174 | } else if (SwingUtilities.isRightMouseButton(e)) { 175 | setSelectionRow(row); 176 | selectedNode = getSelectedNode(); 177 | if (selectedNode != null) 178 | if (selectedNode.isRootPackage()) { 179 | PackageEditDialog packageEditDialog = new PackageEditDialog(mainWindow, selectedNode); 180 | packageEditDialog.setValueChangedListener(this); 181 | packageEditDialog.showDialog(); 182 | } 183 | // popupMenu.show(tree, e.getX(), e.getY()); 184 | } 185 | } 186 | } 187 | 188 | @Override 189 | public void mousePressed(MouseEvent e) { 190 | 191 | } 192 | 193 | @Override 194 | public void mouseReleased(MouseEvent e) { 195 | 196 | } 197 | 198 | @Override 199 | public void mouseEntered(MouseEvent e) { 200 | 201 | } 202 | 203 | @Override 204 | public void mouseExited(MouseEvent e) { 205 | 206 | } 207 | 208 | @Override 209 | public void onEdited(String name, int id) { 210 | try { 211 | if (tableChangedListener != null) { 212 | selectedNode.setName(String.format("%s (%d)", name, id)); 213 | selectedNode.setPackageName(name); 214 | selectedNode.setId(id); 215 | PackageChunk packageChunk = selectedNode.getPackageChunk(); 216 | packageChunk.setPackageName(name); 217 | 218 | tableChangedListener.tableChanged(); 219 | } 220 | } catch (Exception ex) { 221 | ex.printStackTrace(); 222 | new ErrorDialog(mainWindow, ex); 223 | } 224 | } 225 | } 226 | --------------------------------------------------------------------------------