diff --git a/README.md b/README.md
index 6aa8afba..a22db76c 100755
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@
# APKEditor
### Powerful android apk resources editor
-This tool uses [ARSCLib](https://github.com/REAndroid/ARSCLib) to edit any apk resources and has five main features
+This tool uses [ARSCLib](https://github.com/REAndroid/ARSCLib) to edit any apk resources and has six main features
java -jar APKEditor.jar -h
@@ -20,6 +20,7 @@ Usage:
3) m | merge - Merges split apk files from directory or XAPK, APKM, APKS ...
4) x | refactor - Refactors obfuscated resource names
5) p | protect - Protects/Obfuscates apk resource
+ 6) info - Prints information of apk
run with -h to get detailed help about each command
```
@@ -122,7 +123,7 @@ $ java -jar APKEditor.jar x -i input.apk
-#### 5- Protect (⭐NEW⭐)
+#### 5- Protect
Protects apk resources against almost all known decompile/modify tools.
java -jar APKEditor.jar p -i path/to/input.apk
@@ -143,7 +144,30 @@ Protects apk resources against almost all known decompile/modify tools.
```
-
+
+#### 6- Info (⭐NEW⭐)
+Prints/dumps from basic up to detailed information of apk.
+ java -jar APKEditor.jar info -v -resources -i input.apk
+
+ ```ShellSession
+Package name=com.mypackage id=0x7f
+ type string id=1 entryCount=1
+ resource 0x7f010000 string/app_name
+ () My Application
+ (-de) Meine Bewerbung
+ (-ru-rRU) Мое заявление
+ type mipmap id=2 entryCount=1
+ resource 0x7f020000 mipmap/ic_launcher_round
+ () res/mipmap/ic_launcher_round.png
+ type drawable id=3 entryCount=1
+ resource 0x7f030000 drawable/ic_launcher
+ () #006400
+
+```
+
+
+
+
---
***Build executable jar***
diff --git a/libs/ARSCLib.jar b/libs/ARSCLib.jar
index 4e7b1352..b00679a3 100644
Binary files a/libs/ARSCLib.jar and b/libs/ARSCLib.jar differ
diff --git a/src/main/java/com/reandroid/apkeditor/BaseCommand.java b/src/main/java/com/reandroid/apkeditor/BaseCommand.java
index 7209ec9e..b9ef0d5c 100644
--- a/src/main/java/com/reandroid/apkeditor/BaseCommand.java
+++ b/src/main/java/com/reandroid/apkeditor/BaseCommand.java
@@ -1,31 +1,81 @@
- /*
- * Copyright (C) 2022 github.com/REAndroid
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
+/*
+ * Copyright (C) 2022 github.com/REAndroid
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
package com.reandroid.apkeditor;
+import com.reandroid.apk.APKLogger;
import com.reandroid.archive.APKArchive;
import com.reandroid.apk.ApkModule;
+import com.reandroid.commons.utils.log.Logger;
+import java.io.IOException;
import java.util.regex.Pattern;
- public class BaseCommand {
+public class BaseCommand implements APKLogger {
+ private String mLogTag;
+ private boolean mEnableLog;
+ public BaseCommand(){
+ mLogTag = "";
+ mEnableLog = true;
+ }
+ public void run() throws IOException{
- protected static void removeSignature(ApkModule module){
- APKArchive archive = module.getApkArchive();
- archive.removeAll(Pattern.compile("^META-INF/(?!services/).*"));
- archive.remove("stamp-cert-sha256");
- }
+ }
+
+ protected void setLogTag(String tag) {
+ if(tag == null){
+ tag = "";
+ }
+ this.mLogTag = tag;
+ }
+ public void setEnableLog(boolean enableLog) {
+ this.mEnableLog = enableLog;
+ }
+ @Override
+ public void logMessage(String msg) {
+ if(!mEnableLog){
+ return;
+ }
+ Logger.i(mLogTag + msg);
+ }
+ @Override
+ public void logError(String msg, Throwable tr) {
+ if(!mEnableLog){
+ return;
+ }
+ Logger.e(mLogTag + msg, tr);
+ }
+ @Override
+ public void logVerbose(String msg) {
+ if(!mEnableLog){
+ return;
+ }
+ Logger.sameLine(mLogTag + msg);
+ }
+ public void logWarn(String msg) {
+ Logger.e(mLogTag + msg);
+ }
+
+ protected static void clearMeta(ApkModule module){
+ removeSignature(module);
+ module.setApkSignatureBlock(null);
+ }
+ protected static void removeSignature(ApkModule module){
+ APKArchive archive = module.getApkArchive();
+ archive.removeAll(Pattern.compile("^META-INF/.+\\.(([MS]F)|(RSA))"));
+ archive.remove("stamp-cert-sha256");
+ }
}
diff --git a/src/main/java/com/reandroid/apkeditor/Main.java b/src/main/java/com/reandroid/apkeditor/Main.java
index 6a98a4c6..a91902af 100644
--- a/src/main/java/com/reandroid/apkeditor/Main.java
+++ b/src/main/java/com/reandroid/apkeditor/Main.java
@@ -1,25 +1,28 @@
- /*
- * Copyright (C) 2022 github.com/REAndroid
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
+/*
+ * Copyright (C) 2022 github.com/REAndroid
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
package com.reandroid.apkeditor;
+import com.reandroid.apkeditor.cloner.Cloner;
import com.reandroid.apkeditor.compile.Builder;
import com.reandroid.apkeditor.decompile.Decompiler;
+import com.reandroid.apkeditor.info.Info;
import com.reandroid.apkeditor.merge.Merger;
import com.reandroid.apkeditor.protect.Protector;
import com.reandroid.apkeditor.refactor.Refactor;
+import com.reandroid.apkeditor.utils.StringHelper;
import com.reandroid.commons.command.ARGException;
import com.reandroid.apk.xmlencoder.EncodeException;
@@ -77,6 +80,14 @@ private static void execute(String command, String[] args) throws ARGException,
Protector.execute(args);
return;
}
+ if(Cloner.isCommand(command)){
+ Cloner.execute(args);
+ return;
+ }
+ if(Info.isCommand(command)){
+ Info.execute(args);
+ return;
+ }
throw new ARGException("Unknown command: "+command);
}
private static String getHelp(){
@@ -85,18 +96,21 @@ private static String getHelp(){
builder.append("\nUsage: \n");
builder.append(" java -jar ").append(APKEditor.getJarName());
builder.append(" ");
- builder.append("\n commands: ");
- builder.append("\n 1) ").append(Decompiler.ARG_SHORT).append(" | ").append(Decompiler.ARG_LONG);
- builder.append(" - ").append(Decompiler.DESCRIPTION);
- builder.append("\n 2) ").append(Builder.ARG_SHORT).append(" | ").append(Builder.ARG_LONG);
- builder.append(" - ").append(Builder.DESCRIPTION);
- builder.append("\n 3) ").append(Merger.ARG_SHORT).append(" | ").append(Merger.ARG_LONG);
- builder.append(" - ").append(Merger.DESCRIPTION);
- builder.append("\n 4) ").append(Refactor.ARG_SHORT).append(" | ").append(Refactor.ARG_LONG);
- builder.append(" - ").append(Refactor.DESCRIPTION);
- builder.append("\n 5) ").append(Protector.ARG_SHORT).append(" | ").append(Protector.ARG_LONG);
- builder.append(" - ").append(Protector.DESCRIPTION);
- builder.append("\n run with -h to get detailed help about each command");
+ builder.append("\n commands: \n");
+ String[][] table = new String[][]{
+ new String[]{" 1) " + Decompiler.ARG_SHORT + " | " + Decompiler.ARG_LONG, Decompiler.DESCRIPTION},
+ new String[]{" 2) " + Builder.ARG_SHORT + " | " + Builder.ARG_LONG, Builder.DESCRIPTION},
+ new String[]{" 3) " + Merger.ARG_SHORT + " | " + Merger.ARG_LONG, Merger.DESCRIPTION},
+ new String[]{" 4) " + Refactor.ARG_SHORT + " | " + Refactor.ARG_LONG, Refactor.DESCRIPTION},
+ new String[]{" 5) " + Protector.ARG_SHORT + " | " + Protector.ARG_LONG, Protector.DESCRIPTION},
+ //new String[]{" 6) " + Cloner.ARG_SHORT + " | " + Cloner.ARG_LONG, Cloner.DESCRIPTION},
+ new String[]{" 6) " + Info.ARG_SHORT, Info.DESCRIPTION}
+ };
+
+ StringHelper.printTwoColumns(builder, " ", " - ", Options.PRINT_WIDTH, table);
+
+ builder.append("\n\n run with -h to get detailed help about each command\n");
+
return builder.toString();
}
private static String getWelcome(){
diff --git a/src/main/java/com/reandroid/apkeditor/Options.java b/src/main/java/com/reandroid/apkeditor/Options.java
index a3180cd6..a48d2cd4 100644
--- a/src/main/java/com/reandroid/apkeditor/Options.java
+++ b/src/main/java/com/reandroid/apkeditor/Options.java
@@ -46,18 +46,8 @@ protected void parseFrameworkVersion(String[] args) throws ARGException {
}
}
protected void parseType(String[] args) throws ARGException {
- this.type = parseArgValue(ARG_type, true, args);
- if(type == null){
- type = TYPE_JSON;
- return;
- }
- type = type.trim().toLowerCase();
- if(TYPE_JSON.equals(type)
- || TYPE_XML.equals(type)
- || TYPE_SIG.equals(type)){
- return;
- }
- throw new ARGException("Unknown decompile type: "+type);
+ String[] choices = new String[]{TYPE_JSON, TYPE_XML, TYPE_SIG};
+ this.type = parseType(ARG_type, args, choices, TYPE_JSON);
}
protected void parseSignaturesDir(String[] args) throws ARGException {
this.signaturesDirectory = parseFile(ARG_sig, args);
@@ -72,6 +62,32 @@ protected void checkUnknownOptions(String[] args) throws ARGException {
}
throw new ARGException("Unknown option: "+args[0]);
}
+
+ protected String parseType(String argSwitch, String[] args, String[] availableTypes, String def) throws ARGException {
+ String type = parseArgValue(argSwitch, args);
+ if(type == null){
+ return def;
+ }
+ type = type.trim();
+ String typeLower = type.toLowerCase();
+ for(String choice : availableTypes){
+ if(typeLower.equals(choice)){
+ return typeLower;
+ }
+ }
+ StringBuilder builder = new StringBuilder();
+ builder.append("Unknown type: '");
+ builder.append(type);
+ builder.append("' , must be one of {");
+ for(int i = 0; i < availableTypes.length; i++){
+ if(i != 0){
+ builder.append(", ");
+ }
+ builder.append(availableTypes[i]);
+ }
+ builder.append("}");
+ throw new ARGException(builder.toString());
+ }
protected String parseArgValue(String argSwitch, String[] args) throws ARGException {
return parseArgValue(argSwitch, true, args);
}
@@ -132,7 +148,16 @@ protected File parseFile(String argSwitch, String[] args) throws ARGException {
}
return null;
}
- protected boolean containsArg(String argSwitch, boolean ignore_case, String[] args) throws ARGException {
+ protected boolean containsArg(String argSwitch, String[] args) {
+ return containsArg(argSwitch, true, args, false);
+ }
+ protected boolean containsArg(String argSwitch, String[] args, boolean def) {
+ return containsArg(argSwitch, true, args, def);
+ }
+ protected boolean containsArg(String argSwitch, boolean ignore_case, String[] args){
+ return containsArg(argSwitch, ignore_case, args, false);
+ }
+ protected boolean containsArg(String argSwitch, boolean ignore_case, String[] args, boolean def) {
if(ignore_case){
argSwitch=argSwitch.toLowerCase();
}
@@ -151,9 +176,14 @@ protected boolean containsArg(String argSwitch, boolean ignore_case, String[] ar
return true;
}
}
- return false;
+ return def;
}
+ public static final int PRINT_WIDTH = 75;
+
+ protected static final String ARG_ALL_help = "-h|-help";
+ protected static final String ARG_DESC_help = "Prints this help";
+
protected static final String ARG_output="-o";
protected static final String ARG_DESC_output="output path";
protected static final String ARG_input="-i";
@@ -173,12 +203,10 @@ protected boolean containsArg(String argSwitch, boolean ignore_case, String[] ar
protected static final String ARG_DESC_framework_version = "preferred framework version number";
public static final String ARG_type = "-t";
- public static final String ARG_DESC_type = "Decode types: \n1) json \n2) xml \n3) sig \n default=json" +
- "\n * Output directory contains \n a) res package directory(s) name={index number}-{package name}" +
- "\n b) root: directory of raw files like dex, assets, lib ... \n c) AndroidManifest.xml";
-
public static final String TYPE_SIG = "sig";
public static final String TYPE_JSON = "json";
public static final String TYPE_XML = "xml";
+ public static final String TYPE_TEXT = "text";
+ protected static final String LINE = " ------------------------------------------------------------------------";
}
diff --git a/src/main/java/com/reandroid/apkeditor/Util.java b/src/main/java/com/reandroid/apkeditor/Util.java
index 3d25e160..a87e3664 100644
--- a/src/main/java/com/reandroid/apkeditor/Util.java
+++ b/src/main/java/com/reandroid/apkeditor/Util.java
@@ -1,44 +1,59 @@
- /*
- * Copyright (C) 2022 github.com/REAndroid
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
+/*
+ * Copyright (C) 2022 github.com/REAndroid
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
package com.reandroid.apkeditor;
import com.reandroid.apk.ApkModule;
-import com.reandroid.arsc.chunk.PackageBlock;
import com.reandroid.arsc.chunk.TableBlock;
+import com.reandroid.arsc.item.IntegerItem;
import com.reandroid.arsc.item.TableString;
import com.reandroid.arsc.pool.TableStringPool;
import java.io.File;
-import java.io.IOException;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
- public class Util {
+public class Util {
public static boolean isHelp(String[] args){
if(isEmpty(args)){
return true;
}
- String command=args[0];
+ return isHelp(args[0]);
+ }
+ public static boolean containsHelp(String[] args){
+ if(isEmpty(args)){
+ return false;
+ }
+ for(String command : args){
+ if(isHelp(command)){
+ return true;
+ }
+ }
+ return false;
+ }
+ public static boolean isHelp(String command){
+ if(isEmpty(command)){
+ return false;
+ }
command=command.toLowerCase().trim();
return command.equals("-h")
||command.equals("-help")
- ||command.equals("h")
- ||command.equals("help");
+ ||command.equals("--h")
+ ||command.equals("--help");
}
public static String[] trimNull(String[] args){
if(isEmpty(args)){
@@ -184,6 +199,9 @@ private static void addApkEditorInfo(TableBlock tableBlock, String type){
tableString = stringPool.get(count);
}
tableString.set(buildApkEditorInfo(type));
+ IntegerItem dummyReference = new IntegerItem();
+ dummyReference.set(tableString.getIndex());
+ tableString.addReference(dummyReference);
}
private static String buildApkEditorInfo(String type){
StringBuilder builder = new StringBuilder();
diff --git a/src/main/java/com/reandroid/apkeditor/cloner/Cloner.java b/src/main/java/com/reandroid/apkeditor/cloner/Cloner.java
new file mode 100644
index 00000000..4b0718c5
--- /dev/null
+++ b/src/main/java/com/reandroid/apkeditor/cloner/Cloner.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2022 github.com/REAndroid
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.reandroid.apkeditor.cloner;
+
+import com.reandroid.apkeditor.APKEditor;
+import com.reandroid.apkeditor.BaseCommand;
+import com.reandroid.apkeditor.Util;
+import com.reandroid.commons.command.ARGException;
+
+import java.io.IOException;
+
+public class Cloner extends BaseCommand {
+ private final ClonerOptions options;
+ public Cloner(ClonerOptions options){
+ this.options = options;
+ }
+ @Override
+ public void run() throws IOException{
+ logWarn("This feature not implemented, follow updates on: " + APKEditor.getRepo());
+ }
+
+ public static void execute(String[] args) throws ARGException, IOException {
+ if(Util.isHelp(args)){
+ throw new ARGException(ClonerOptions.getHelp());
+ }
+ ClonerOptions option = new ClonerOptions();
+ option.parse(args);
+ Cloner cloner = new Cloner(option);
+ cloner.run();
+ }
+
+ public static boolean isCommand(String command){
+ if(Util.isEmpty(command)){
+ return false;
+ }
+ command = command.toLowerCase().trim();
+ return command.equals(ARG_LONG);
+ //return command.equals(ARG_SHORT) || command.equals(ARG_LONG);
+ }
+
+ public static final String ARG_SHORT = "c";
+ public static final String ARG_LONG = "clone";
+
+ public static final String DESCRIPTION = "Clones apk";
+}
diff --git a/src/main/java/com/reandroid/apkeditor/cloner/ClonerOptions.java b/src/main/java/com/reandroid/apkeditor/cloner/ClonerOptions.java
new file mode 100644
index 00000000..6f112aad
--- /dev/null
+++ b/src/main/java/com/reandroid/apkeditor/cloner/ClonerOptions.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2022 github.com/REAndroid
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.reandroid.apkeditor.cloner;
+
+import com.reandroid.apkeditor.APKEditor;
+import com.reandroid.apkeditor.Options;
+import com.reandroid.apkeditor.Util;
+import com.reandroid.apkeditor.utils.StringHelper;
+import com.reandroid.commons.command.ARGException;
+
+import java.io.File;
+
+public class ClonerOptions extends Options {
+ public String packageName;
+ public String appName;
+ public String appIcon;
+ public boolean keepAuth;
+ public ClonerOptions(){
+ super();
+ }
+ @Override
+ public void parse(String[] args) throws ARGException {
+ parseInput(args);
+
+ packageName = parseArgValue(ARG_package, args);
+ appName = parseArgValue(ARG_app_name, args);
+ appIcon = parseArgValue(ARG_app_icon, args);
+ keepAuth = containsArg(ARG_app_icon, args);
+
+ parseHelp(args);
+
+ checkUnknownOptions(args);
+ }
+ private void parseInput(String[] args) throws ARGException {
+ this.inputFile = null;
+ File file = parseFile(ARG_input, args);
+ if(file == null){
+ throw new ARGException("Missing input apk! Specify with " + ARG_input);
+ }
+ if(!file.isFile()){
+ throw new ARGException("No such file: "+file);
+ }
+ this.inputFile = file;
+ }
+ private void parseHelp(String[] args) throws ARGException {
+ if(!Util.containsHelp(args)){
+ return;
+ }
+ throw new ARGException(getHelp());
+ }
+
+ public static String getHelp(){
+ StringBuilder builder=new StringBuilder();
+ builder.append(Cloner.DESCRIPTION);
+ builder.append("\nOptions:\n");
+ String[][] table=new String[][]{
+ new String[]{ARG_input, ARG_DESC_input},
+ new String[]{ARG_output, ARG_DESC_output},
+ new String[]{ARG_package, ARG_DESC_package},
+ new String[]{ARG_app_name, ARG_DESC_app_name},
+ new String[]{ARG_app_icon, ARG_DESC_app_icon}
+ };
+ StringHelper.printTwoColumns(builder, " ", PRINT_WIDTH, table);
+ builder.append("\n\nFlags:\n");
+ table=new String[][]{
+ new String[]{ARG_keep_auth, ARG_DESC_keep_auth},
+ new String[]{" ", " "},
+ new String[]{ARG_ALL_help, ARG_DESC_help}
+ };
+ StringHelper.printTwoColumns(builder, " ", PRINT_WIDTH, table);
+
+ builder.append("\n").append(Options.LINE);
+
+ String jar = APKEditor.getJarName();
+
+ builder.append("\n\nExample-1:");
+ builder.append("\n java -jar ").append(jar).append(" ").append(Cloner.ARG_SHORT).append(" ")
+ .append(ARG_input).append(" file.apk");
+
+ return builder.toString();
+ }
+
+ private static final String ARG_package = "-package";
+ private static final String ARG_DESC_package = "Package name.";
+
+ private static final String ARG_app_name = "-app-name";
+ private static final String ARG_DESC_app_name = "Application name.";
+
+ private static final String ARG_app_icon = "-app-icon";
+ private static final String ARG_DESC_app_icon = "Application icon. File path of app icon(s).";
+
+ private static final String ARG_keep_auth = "-keep-auth";
+ private static final String ARG_DESC_keep_auth = "Do not rename authorities as per package. \n *Applies only when option -package used.";
+
+}
diff --git a/src/main/java/com/reandroid/apkeditor/common/AndroidManifestHelper.java b/src/main/java/com/reandroid/apkeditor/common/AndroidManifestHelper.java
index a0c6454f..0e5f892c 100644
--- a/src/main/java/com/reandroid/apkeditor/common/AndroidManifestHelper.java
+++ b/src/main/java/com/reandroid/apkeditor/common/AndroidManifestHelper.java
@@ -1,4 +1,4 @@
- /*
+/*
* Copyright (C) 2022 github.com/REAndroid
*
* Licensed under the Apache License, Version 2.0 (the "License");
@@ -15,11 +15,9 @@
*/
package com.reandroid.apkeditor.common;
-import com.reandroid.arsc.array.ResXmlAttributeArray;
import com.reandroid.arsc.chunk.xml.AndroidManifestBlock;
import com.reandroid.arsc.chunk.xml.ResXmlAttribute;
import com.reandroid.arsc.chunk.xml.ResXmlElement;
-import com.reandroid.arsc.chunk.xml.ResXmlStartElement;
import com.reandroid.arsc.value.ValueType;
import java.util.ArrayList;
@@ -27,22 +25,20 @@
public class AndroidManifestHelper {
public static List listSplitRequired(ResXmlElement parentElement){
- List results=new ArrayList<>();
- if(parentElement==null){
+ List results = new ArrayList<>();
+ if(parentElement == null){
return results;
}
- List metaDataList = parentElement.listElements(AndroidManifestBlock.TAG_meta_data);
+ List metaDataList = parentElement
+ .listElements(AndroidManifestBlock.TAG_meta_data);
+
for(ResXmlElement metaData:metaDataList){
- ResXmlAttribute nameAttribute = metaData.getStartElement()
- .getAttribute(AndroidManifestBlock.ID_name);
- if(nameAttribute==null){
+ ResXmlAttribute nameAttribute = metaData
+ .searchAttributeByResourceId(AndroidManifestBlock.ID_name);
+ if(nameAttribute == null){
continue;
}
if(nameAttribute.getValueType() != ValueType.STRING){
- /*
- * TODO: could be reference ,
- * thus we need TableBlock/EntryStore to resolve string value.
- */
continue;
}
String value = nameAttribute.getValueAsString();
@@ -54,18 +50,16 @@ public static List listSplitRequired(ResXmlElement parentElement)
return results;
}
public static boolean removeApplicationAttribute(AndroidManifestBlock manifest, int resId){
- ResXmlElement app = manifest.getApplicationElement();
- if(app==null){
+ ResXmlElement applicationElement = manifest.getApplicationElement();
+ if(applicationElement == null){
return true;
}
- ResXmlStartElement start = app.getStartElement();
- ResXmlAttribute attr = start.getAttribute(resId);
- if(attr==null){
+ ResXmlAttribute attribute = applicationElement
+ .searchAttributeByResourceId(resId);
+ if(attribute == null){
return false;
}
- ResXmlAttributeArray array = start.getResXmlAttributeArray();
- array.remove(attr);
- manifest.refresh();
+ applicationElement.removeAttribute(attribute);
return true;
}
}
diff --git a/src/main/java/com/reandroid/apkeditor/compile/BuildOptions.java b/src/main/java/com/reandroid/apkeditor/compile/BuildOptions.java
index 1c8f054e..2cd63973 100644
--- a/src/main/java/com/reandroid/apkeditor/compile/BuildOptions.java
+++ b/src/main/java/com/reandroid/apkeditor/compile/BuildOptions.java
@@ -117,13 +117,13 @@ public static String getHelp(){
new String[]{ARG_sig, ARG_DESC_sig},
new String[]{ARG_resDir, ARG_DESC_resDir}
};
- StringHelper.printTwoColumns(builder, " ", 75, table);
+ StringHelper.printTwoColumns(builder, " ", Options.PRINT_WIDTH, table);
builder.append("\nFlags:\n");
table=new String[][]{
new String[]{ARG_force, ARG_DESC_force},
new String[]{ARG_validate_res_dir, ARG_DESC_validate_res_dir}
};
- StringHelper.printTwoColumns(builder, " ", 75, table);
+ StringHelper.printTwoColumns(builder, " ", Options.PRINT_WIDTH, table);
String jar = APKEditor.getJarName();
builder.append("\n\nExample-1:");
builder.append("\n java -jar ").append(jar).append(" ").append(Builder.ARG_SHORT).append(" ")
diff --git a/src/main/java/com/reandroid/apkeditor/decompile/DecompileOptions.java b/src/main/java/com/reandroid/apkeditor/decompile/DecompileOptions.java
index 681bf9ed..16358237 100644
--- a/src/main/java/com/reandroid/apkeditor/decompile/DecompileOptions.java
+++ b/src/main/java/com/reandroid/apkeditor/decompile/DecompileOptions.java
@@ -128,14 +128,14 @@ public static String getHelp(){
new String[]{ARG_type, ARG_DESC_type},
new String[]{ARG_resDir, ARG_DESC_resDir}
};
- StringHelper.printTwoColumns(builder, " ", 75, table);
+ StringHelper.printTwoColumns(builder, " ", Options.PRINT_WIDTH, table);
builder.append("\nFlags:\n");
table=new String[][]{
new String[]{ARG_force, ARG_DESC_force},
new String[]{ARG_split_resources, ARG_DESC_split_resources},
new String[]{ARG_validate_res_dir, ARG_DESC_validate_res_dir}
};
- StringHelper.printTwoColumns(builder, " ", 75, table);
+ StringHelper.printTwoColumns(builder, " ", Options.PRINT_WIDTH, table);
String jar = APKEditor.getJarName();
builder.append("\n\nExample-1:");
builder.append("\n java -jar ").append(jar).append(" ").append(Builder.ARG_SHORT).append(" ")
diff --git a/src/main/java/com/reandroid/apkeditor/info/Info.java b/src/main/java/com/reandroid/apkeditor/info/Info.java
new file mode 100644
index 00000000..7c0ed3ae
--- /dev/null
+++ b/src/main/java/com/reandroid/apkeditor/info/Info.java
@@ -0,0 +1,421 @@
+/*
+ * Copyright (C) 2022 github.com/REAndroid
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.reandroid.apkeditor.info;
+
+import com.reandroid.apk.ApkModule;
+import com.reandroid.apkeditor.BaseCommand;
+import com.reandroid.apkeditor.Util;
+import com.reandroid.arsc.chunk.PackageBlock;
+import com.reandroid.arsc.chunk.TableBlock;
+import com.reandroid.arsc.chunk.xml.AndroidManifestBlock;
+import com.reandroid.arsc.chunk.xml.ResXmlAttribute;
+import com.reandroid.arsc.chunk.xml.ResXmlElement;
+import com.reandroid.arsc.coder.EncodeResult;
+import com.reandroid.arsc.coder.ReferenceString;
+import com.reandroid.arsc.coder.ValueCoder;
+import com.reandroid.arsc.group.EntryGroup;
+import com.reandroid.arsc.util.HexUtil;
+import com.reandroid.arsc.value.AttributeDataFormat;
+import com.reandroid.arsc.value.Entry;
+import com.reandroid.arsc.value.ResValue;
+import com.reandroid.arsc.value.ValueType;
+import com.reandroid.commons.command.ARGException;
+
+import java.io.*;
+import java.util.*;
+
+public class Info extends BaseCommand {
+ private final InfoOptions options;
+ private InfoWriter mInfoWriter;
+ public Info(InfoOptions options){
+ super();
+ this.options = options;
+ super.setLogTag(LOG_TAG_INFO);
+ super.setEnableLog(options.outputFile != null);
+ }
+ @Override
+ public void run() throws IOException{
+ setEnableLog(options.outputFile != null);
+ logMessage("Loading: " + options.inputFile);
+ ApkModule apkModule = ApkModule.loadApkFile(options.inputFile);
+ String msg = Util.isProtected(apkModule);
+ if(msg != null){
+ logMessage(msg);
+ return;
+ }
+ apkModule.setAPKLogger(this);
+ apkModule.setLoadDefaultFramework(options.verbose);
+ File out = options.outputFile;
+ if(out != null){
+ logMessage("Writing ...");
+ }
+ print(apkModule);
+ flush();
+ close();
+ if(out != null){
+ logMessage("Saved to: " + out);
+ }
+ }
+ private void print(ApkModule apkModule) throws IOException {
+ printSourceFile();
+
+ printPackage(apkModule);
+ AndroidManifestBlock manifest = apkModule.getAndroidManifestBlock();
+ printVersionCode(manifest);
+ printVersionName(manifest);
+
+ printAppName(apkModule);
+ printAppIcon(apkModule);
+ printAppRoundIcon(apkModule);
+ printAppClass(apkModule);
+ printActivities(apkModule);
+ printUsesPermissions(apkModule);
+
+ printResList(apkModule);
+
+ printResources(apkModule);
+ }
+ private void printSourceFile() throws IOException {
+ if(options.outputFile == null){
+ return;
+ }
+ if(options.verbose || !options.resources){
+ getInfoWriter().writeNameValue("source-file",
+ options.inputFile.getAbsolutePath());
+ }
+ }
+ private void printResources(ApkModule apkModule) throws IOException {
+ if(!options.resources){
+ return;
+ }
+ if(!apkModule.hasTableBlock()){
+ return;
+ }
+ TableBlock tableBlock = apkModule.getTableBlock();
+ InfoWriter infoWriter = getInfoWriter();
+ boolean writeEntries = options.verbose;
+ for(PackageBlock packageBlock : tableBlock.listPackages()){
+ infoWriter.writeResources(packageBlock, options.typeFilterList, writeEntries);
+ }
+ }
+ private void printResList(ApkModule apkModule) throws IOException {
+ if(options.resList.size() == 0){
+ return;
+ }
+ if(!apkModule.hasTableBlock()){
+ return;
+ }
+ for(String res : options.resList){
+ printRes(apkModule, res);
+ }
+ }
+ private void printRes(ApkModule apkModule, String res) throws IOException {
+ if(res == null || res.length() < 3){
+ return;
+ }
+ if(res.startsWith("@0x")){
+ res = res.substring(1);
+ }
+ EncodeResult encodeResult = ValueCoder.encode(res, AttributeDataFormat.INTEGER.valueTypes());
+ if(encodeResult != null){
+ printEntries(apkModule, "resource", encodeResult.value);
+ return;
+ }
+ ReferenceString referenceString = ReferenceString.parseReference(res);
+ if(referenceString == null){
+ logWarn("WARN: Invalid resource: " + res);
+ return;
+ }
+ TableBlock tableBlock = apkModule.getTableBlock();
+ for(PackageBlock packageBlock : tableBlock.listPackages()){
+ EntryGroup entryGroup = packageBlock
+ .getEntryGroup(referenceString.type, referenceString.name);
+ if(entryGroup == null){
+ continue;
+ }
+ printEntries(apkModule, "resource", entryGroup.getResourceId());
+ return;
+ }
+ logMessage("WARN: resource not found: " + res);
+ }
+ private void printPackage(ApkModule apkModule) throws IOException {
+ if(!options.packageName){
+ return;
+ }
+ AndroidManifestBlock manifest = apkModule.getAndroidManifestBlock();
+ if(manifest != null){
+ getInfoWriter().writeNameValue("package" , manifest.getPackageName());
+ }
+ if(!options.verbose || !apkModule.hasTableBlock()){
+ return;
+ }
+ TableBlock tableBlock = apkModule.getTableBlock();
+ InfoWriter infoWriter = getInfoWriter();
+ infoWriter.writePackageNames(tableBlock.listPackages());
+ }
+ private void printVersionCode(AndroidManifestBlock manifest) throws IOException {
+ if(!options.versionCode || manifest == null){
+ return;
+ }
+ getInfoWriter().writeNameValue("VersionCode" , manifest.getVersionCode());
+ }
+ private void printVersionName(AndroidManifestBlock manifest) throws IOException {
+ if(!options.versionName || manifest == null){
+ return;
+ }
+ getInfoWriter().writeNameValue("VersionName" , manifest.getVersionName());
+ }
+ private void printAppName(ApkModule apkModule) throws IOException {
+ if(!options.appName){
+ return;
+ }
+ AndroidManifestBlock manifest = apkModule.getAndroidManifestBlock();
+ ResXmlElement application = manifest.getApplicationElement();
+ ResXmlAttribute attributeLabel = application
+ .searchAttributeByResourceId(AndroidManifestBlock.ID_label);
+ if(attributeLabel == null){
+ return;
+ }
+ if(attributeLabel.getValueType() == ValueType.STRING){
+ getInfoWriter().writeNameValue("AppName", attributeLabel.getValueAsString());
+ return;
+ }
+ int resourceId = attributeLabel.getData();
+ printEntries(apkModule, "AppName", resourceId);
+ }
+ private void printAppIcon(ApkModule apkModule) throws IOException {
+ if(!options.appIcon){
+ return;
+ }
+ AndroidManifestBlock manifest = apkModule.getAndroidManifestBlock();
+ ResXmlElement application = manifest.getApplicationElement();
+ ResXmlAttribute attribute = application
+ .searchAttributeByResourceId(AndroidManifestBlock.ID_icon);
+ if(attribute == null){
+ return;
+ }
+ if(attribute.getValueType() == ValueType.STRING){
+ getInfoWriter().writeNameValue("AppIcon", attribute.getValueAsString());
+ return;
+ }
+ int resourceId = attribute.getData();
+ printEntries(apkModule, "AppIcon", resourceId);
+ }
+ private void printAppRoundIcon(ApkModule apkModule) throws IOException {
+ if(!options.appRoundIcon){
+ return;
+ }
+ AndroidManifestBlock manifest = apkModule.getAndroidManifestBlock();
+ ResXmlElement application = manifest.getApplicationElement();
+ int id_roundIcon = 0x0101052c;
+ ResXmlAttribute attribute = application
+ .searchAttributeByResourceId(id_roundIcon);
+ if(attribute == null){
+ return;
+ }
+ if(attribute.getValueType() == ValueType.STRING){
+ getInfoWriter().writeNameValue("AppRoundIcon", attribute.getValueAsString());
+ return;
+ }
+ int resourceId = attribute.getData();
+ printEntries(apkModule, "AppRoundIcon", resourceId);
+ }
+ private void printUsesPermissions(ApkModule apkModule) throws IOException {
+ if(!options.permissions || !options.verbose){
+ return;
+ }
+ AndroidManifestBlock manifest = apkModule.getAndroidManifestBlock();
+ if(manifest == null){
+ return;
+ }
+ List usesPermissions = manifest.getUsesPermissions();
+ if(usesPermissions.size() == 0){
+ return;
+ }
+ //printLine("Uses permission (" + usesPermissions.size() + ")");
+ String tag = AndroidManifestBlock.TAG_uses_permission;
+ InfoWriter infoWriter = getInfoWriter();
+ infoWriter.writeArray(tag, usesPermissions.toArray(new String[0]));
+ }
+ private void printActivities(ApkModule apkModule) throws IOException {
+ if(!options.activities){
+ return;
+ }
+ AndroidManifestBlock manifest = apkModule.getAndroidManifestBlock();
+ if(manifest == null){
+ return;
+ }
+ List activityList = manifest.listActivities(true);
+ if(activityList.size() == 0){
+ return;
+ }
+ ResXmlElement main = manifest.getMainActivity();
+ if(main != null){
+ String value = getValueOfName(main);
+ getInfoWriter().writeNameValue("activity-main", value);
+ }
+ if(!options.verbose){
+ return;
+ }
+ String[] activityNames = new String[activityList.size()];
+ for(int i = 0; i < activityList.size(); i++){
+ ResXmlElement activity = activityList.get(i);
+ activityNames[i] = getValueOfName(activity);
+ }
+ InfoWriter infoWriter = getInfoWriter();
+ infoWriter.writeArray("activities", activityNames);
+ }
+ private void printAppClass(ApkModule apkModule) throws IOException {
+ if(!options.appClass){
+ return;
+ }
+ AndroidManifestBlock manifest = apkModule.getAndroidManifestBlock();
+ if(manifest == null){
+ return;
+ }
+ ResXmlElement applicationElement = manifest.getApplicationElement();
+ if(applicationElement == null){
+ return;
+ }
+ String value = getValueOfName(applicationElement);
+ if(value != null){
+ getInfoWriter().writeNameValue("application-class", value);
+ }
+ }
+ private String getValueOfName(ResXmlElement element){
+ ResXmlAttribute attribute = element
+ .searchAttributeByResourceId(AndroidManifestBlock.ID_name);
+ if(attribute == null){
+ return null;
+ }
+ return attribute.getValueAsString();
+ }
+ private void printEntries(ApkModule apkModule, String varName, int resourceId) throws IOException {
+ TableBlock tableBlock = apkModule.getTableBlock();
+ InfoWriter infoWriter = getInfoWriter();
+ if(tableBlock == null){
+ infoWriter.writeNameValue(varName, HexUtil.toHex8( "@0x", resourceId));
+ return;
+ }
+ List entryList = tableBlock.resolveReference(resourceId);
+ if(entryList.size() == 0){
+ logWarn("WARN: Can't find resource: " + HexUtil.toHex8("@0x", resourceId));
+ //infoWriter.writeNameValue(varName, HexUtil.toHex8("@0x", resourceId));
+ return;
+ }
+ entryList = sortEntries(entryList);
+ if(!options.verbose){
+ infoWriter.writeNameValue(varName, getValueAsString(entryList.get(0)));
+ return;
+ }
+ infoWriter.writeEntries(varName, entryList);
+ }
+ private String getValueAsString(Entry entry){
+ ResValue resValue = entry.getResValue();
+ if(resValue == null){
+ return "";
+ }
+ ValueType valueType = resValue.getValueType();
+ if(valueType == ValueType.STRING){
+ return resValue.getValueAsString();
+ }
+ String decoded = ValueCoder.decode(valueType, resValue.getData());
+ if(decoded != null){
+ return decoded;
+ }
+ return HexUtil.toHex8("@0x", resValue.getData());
+ }
+ private InfoWriter getInfoWriter() throws IOException{
+ if(mInfoWriter != null){
+ return mInfoWriter;
+ }
+ Writer writer = createWriter();
+ InfoWriter infoWriter;
+ if(InfoOptions.TYPE_JSON.equals(options.type)){
+ infoWriter = new InfoWriterJson(writer);
+ }else if(InfoOptions.TYPE_XML.equals(options.type)){
+ infoWriter = new InfoWriterXml(writer);
+ }else {
+ infoWriter = new InfoWriterText(writer);
+ }
+ mInfoWriter = infoWriter;
+ return mInfoWriter;
+ }
+ private Writer createWriter() throws IOException{
+ File file = options.outputFile;
+ if(file == null){
+ return new PrintWriter(System.out);
+ }
+ File dir = file.getParentFile();
+ if(dir != null && !dir.exists()){
+ dir.mkdirs();
+ }
+ return new OutputStreamWriter(new FileOutputStream(file));
+ }
+ private void flush() throws IOException {
+ InfoWriter writer = this.mInfoWriter;
+ if(writer != null){
+ writer.flush();
+ }
+ }
+ private void close() throws IOException {
+ InfoWriter writer = this.mInfoWriter;
+ if(writer != null){
+ writer.close();
+ mInfoWriter = null;
+ }
+ }
+
+ public static void execute(String[] args) throws ARGException, IOException {
+ if(Util.isHelp(args)){
+ throw new ARGException(InfoOptions.getHelp());
+ }
+ InfoOptions option = new InfoOptions();
+ option.parse(args);
+ Info info = new Info(option);
+ info.run();
+ }
+
+ private static List sortEntries(Collection entryCollection) {
+ ArrayList results;
+ if(entryCollection instanceof ArrayList){
+ results = (ArrayList) entryCollection;
+ }else {
+ results = new ArrayList<>(entryCollection);
+ }
+ Comparator cmp = new Comparator() {
+ @Override
+ public int compare(Entry entry1, Entry entry2) {
+ return entry1.getResConfig().compareTo(entry2.getResConfig());
+ }
+ };
+ results.sort(cmp);
+ return results;
+ }
+
+ public static boolean isCommand(String command){
+ if(Util.isEmpty(command)){
+ return false;
+ }
+ command=command.toLowerCase().trim();
+ return command.equals(ARG_SHORT);
+ }
+
+ public static final String ARG_SHORT = "info";
+ public static final String DESCRIPTION = "Prints information of apk";
+
+ private static final String LOG_TAG_INFO = "[INFO] ";
+}
diff --git a/src/main/java/com/reandroid/apkeditor/info/InfoOptions.java b/src/main/java/com/reandroid/apkeditor/info/InfoOptions.java
new file mode 100644
index 00000000..d5001f5a
--- /dev/null
+++ b/src/main/java/com/reandroid/apkeditor/info/InfoOptions.java
@@ -0,0 +1,256 @@
+/*
+ * Copyright (C) 2022 github.com/REAndroid
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.reandroid.apkeditor.info;
+
+import com.reandroid.apkeditor.APKEditor;
+import com.reandroid.apkeditor.Options;
+import com.reandroid.apkeditor.Util;
+import com.reandroid.apkeditor.utils.StringHelper;
+import com.reandroid.commons.command.ARGException;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
+
+public class InfoOptions extends Options {
+ public boolean verbose = true;
+ public boolean packageName = true;
+ public boolean versionCode = true;
+ public boolean versionName = true;
+ public boolean appName = true;
+ public boolean appIcon = true;
+ public boolean appRoundIcon = true;
+ public boolean permissions = true;
+ public boolean appClass = true;
+ public boolean activities = true;
+ public final List resList;
+ public boolean resources = false;
+ public final List typeFilterList;
+ public InfoOptions(){
+ super();
+ this.resList = new ArrayList<>();
+ this.typeFilterList = new ArrayList<>();
+ }
+ @Override
+ public void parse(String[] args) throws ARGException {
+ parseInput(args);
+
+ verbose = containsArg(ARG_verbose, args);
+ type = parseType(ARG_type, args, availableTypes, TYPE_TEXT);
+
+ parseOutput(args);
+
+ initializeDefaults(args);
+
+ parseResList(args);
+
+ parseResourceFilterList(args);
+
+ packageName = containsArg(ARG_package, args, packageName);
+ versionCode = containsArg(ARG_version_code, args, versionCode);
+ versionName = containsArg(ARG_version_name, args, versionName);
+ appName = containsArg(ARG_app_name, args, appName);
+ appIcon = containsArg(ARG_app_icon, args, appIcon);
+ appRoundIcon = containsArg(ARG_app_round_icon, args, appRoundIcon);
+ permissions = containsArg(ARG_permissions, args, permissions);
+ appClass = containsArg(ARG_app_class, args, appClass);
+ activities = containsArg(ARG_activities, args, activities);
+ resources = containsArg(ARG_resources, args, false);
+
+
+
+ parseHelp(args);
+
+ super.checkUnknownOptions(args);
+ }
+ private void parseOutput(String[] args) throws ARGException {
+ this.outputFile = null;
+ File file = parseFile(ARG_output, args);
+ if(file == null){
+ return;
+ }
+ String name = file.getName().toLowerCase();
+ String ext;
+ if(TYPE_TEXT.equals(type)){
+ if(name.endsWith(".text")){
+ ext = ".text";
+ }else {
+ ext = ".txt";
+ }
+ }else {
+ ext = "." + type.toLowerCase();
+ }
+ if(!name.endsWith(ext)){
+ throw new ARGException("Invalid file extension! Expected = \""
+ + ext + "\", " + file);
+ }
+ this.outputFile = file;
+ }
+ private void parseResourceFilterList(String[] args) throws ARGException {
+ String filter;
+ while (( filter = parseArgValue(ARG_filter_type, args)) != null){
+ typeFilterList.add(filter);
+ }
+ }
+ private void parseResList(String[] args) throws ARGException {
+ String res;
+ while (( res = parseArgValue(ARG_res, args)) != null){
+ resList.add(res);
+ verbose = true;
+ }
+ }
+ private void initializeDefaults(String[] args){
+ resources = false;
+ if(!Util.isEmpty(args)){
+ packageName = false;
+ versionCode = false;
+ versionName = false;
+ appName = false;
+ appIcon = false;
+ appRoundIcon = false;
+ permissions = false;
+ activities = false;
+ appClass = false;
+ }
+ }
+ private void parseInput(String[] args) throws ARGException {
+ this.inputFile = null;
+ File file = parseFile(ARG_input, args);
+ if(file == null){
+ throw new ARGException("Missing input apk! Specify with " + ARG_input);
+ }
+ if(!file.isFile()){
+ throw new ARGException("No such file: "+file);
+ }
+ this.inputFile = file;
+ }
+ private void parseHelp(String[] args) throws ARGException {
+ if(!Util.containsHelp(args)){
+ return;
+ }
+ throw new ARGException(getHelp());
+ }
+ public static String getHelp(){
+ StringBuilder builder=new StringBuilder();
+ builder.append(Info.DESCRIPTION);
+ builder.append("\nOptions:\n");
+ String[][] table=new String[][]{
+ new String[]{ARG_input, ARG_DESC_input},
+ new String[]{ARG_type, ARG_DESC_type},
+ new String[]{ARG_output, ARG_DESC_output},
+ new String[]{ARG_res, ARG_DESC_res},
+ new String[]{ARG_filter_type, ARG_DESC_filter_type},
+ };
+ StringHelper.printTwoColumns(builder, " ", PRINT_WIDTH, table);
+ builder.append("\n\nFlags:\n");
+ table=new String[][]{
+ new String[]{ARG_verbose, ARG_DESC_verbose},
+ new String[]{ARG_package, ARG_DESC_package},
+ new String[]{ARG_version_code, ARG_DESC_version_code},
+ new String[]{ARG_version_name, ARG_DESC_version_name},
+ new String[]{ARG_app_name, ARG_DESC_app_name},
+ new String[]{ARG_app_icon, ARG_DESC_app_icon},
+ new String[]{ARG_app_round_icon, ARG_DESC_app_round_icon},
+ new String[]{ARG_app_class, ARG_DESC_app_class},
+ new String[]{ARG_permissions, ARG_DESC_permissions},
+ new String[]{ARG_activities, ARG_DESC_activities},
+ new String[]{ARG_resources, ARG_DESC_resources},
+ new String[]{" ", " "},
+ new String[]{ARG_ALL_help, ARG_DESC_help}
+ };
+ StringHelper.printTwoColumns(builder, " ", PRINT_WIDTH, table);
+
+ builder.append("\n").append(Options.LINE);
+
+ String jar = APKEditor.getJarName();
+
+ builder.append("\n\nExample-1:");
+ builder.append("\n java -jar ").append(jar).append(" ").append(Info.ARG_SHORT).append(" ")
+ .append(ARG_input).append(" file.apk");
+
+ builder.append("\n\nExample-2:");
+ builder.append("\n java -jar ").append(jar).append(" ").append(Info.ARG_SHORT).append(" ")
+ .append(ARG_input).append(" file.apk");
+ builder.append(" ").append(ARG_type).append(" ").append(TYPE_JSON.toUpperCase());
+ builder.append(" ").append(ARG_verbose);
+ builder.append(" ").append(ARG_output).append(" info_file.json");
+
+ builder.append("\n\nExample-3:");
+ builder.append("\n java -jar ").append(jar).append(" ").append(Info.ARG_SHORT).append(" ")
+ .append(ARG_input).append(" file.apk ")
+ .append(ARG_resources)
+ .append(" ").append(ARG_filter_type).append(" ").append("mipmap")
+ .append(" ").append(ARG_filter_type).append(" ").append("drawable");
+
+ builder.append("\n\nExample-4:");
+ builder.append("\n java -jar ").append(jar).append(" ").append(Info.ARG_SHORT).append(" ")
+ .append(ARG_input).append(" file.apk ")
+ .append(ARG_verbose)
+ .append(" ").append(ARG_res).append(" ").append("@string/app_name")
+ .append(" ").append(ARG_res).append(" ").append("0x7f010000");
+
+ return builder.toString();
+ }
+
+ protected static final String ARG_output = "-o";
+ protected static final String ARG_DESC_output = "Output path, default is print to std stream.";
+
+ private static final String ARG_type = "-t";
+ private static final String ARG_DESC_type = "Print type, options:\n 1) TEXT\n 2) JSON\n 3) XML\n default=TEXT";
+
+ private static final String ARG_verbose = "-v";
+ private static final String ARG_DESC_verbose = "Verbose mode.";
+
+ private static final String ARG_package = "-package";
+ private static final String ARG_DESC_package = "Package name(s) from manifest and if verbose mode, prints resource table packages.";
+
+ private static final String ARG_version_code = "-version-code";
+ private static final String ARG_DESC_version_code = "App version code.";
+ private static final String ARG_version_name = "-version-name";
+ private static final String ARG_DESC_version_name = "App version name.";
+
+ private static final String ARG_app_name = "-app-name";
+ private static final String ARG_DESC_app_name = "App name. If verbose mode, prints all configurations.";
+ private static final String ARG_app_icon = "-app-icon";
+ private static final String ARG_DESC_app_icon = "App icon path/value. If verbose mode, prints all configurations.";
+ private static final String ARG_app_round_icon = "-app-round-icon";
+ private static final String ARG_DESC_app_round_icon = "App round icon path/value. If verbose mode, prints all configurations.";
+ private static final String ARG_permissions = "-permissions";
+ private static final String ARG_DESC_permissions = "Permissions.";
+ private static final String ARG_app_class = "-app-class";
+ private static final String ARG_DESC_app_class = "Application class name.";
+ private static final String ARG_activities = "-activities";
+ private static final String ARG_DESC_activities = "Prints main activity class name. If verbose mode, " +
+ "prints all declared activities including .";
+
+ private static final String ARG_res = "-res";
+ private static final String ARG_DESC_res = "Prints resource entries specified by either of:" +
+ "\n 1) Hex or decimal resource id.\n 2) Full resource name e.g @string/app_name." +
+ "\n *Can be multiple";
+
+
+ private static final String ARG_resources = "-resources";
+ private static final String ARG_DESC_resources = "Prints all resources.";
+
+
+ private static final String ARG_filter_type = "-filter-type";
+ private static final String ARG_DESC_filter_type = "Prints only the specified resource type names" +
+ "\n *This applies only when flag '-resources' used." +
+ "\n *Can be multiple";
+
+
+ private static final String[] availableTypes = new String[]{TYPE_TEXT, TYPE_JSON, TYPE_XML};
+}
diff --git a/src/main/java/com/reandroid/apkeditor/info/InfoWriter.java b/src/main/java/com/reandroid/apkeditor/info/InfoWriter.java
new file mode 100644
index 00000000..4d0b9420
--- /dev/null
+++ b/src/main/java/com/reandroid/apkeditor/info/InfoWriter.java
@@ -0,0 +1,140 @@
+/*
+ * Copyright (C) 2022 github.com/REAndroid
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.reandroid.apkeditor.info;
+
+import com.reandroid.arsc.chunk.PackageBlock;
+import com.reandroid.arsc.coder.ValueCoder;
+import com.reandroid.arsc.container.SpecTypePair;
+import com.reandroid.arsc.group.EntryGroup;
+import com.reandroid.arsc.util.HexUtil;
+import com.reandroid.arsc.value.*;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.io.Writer;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.List;
+
+public abstract class InfoWriter implements Closeable {
+ private final Writer writer;
+ public InfoWriter(Writer writer){
+ this.writer = writer;
+ }
+
+ public void writeResources(PackageBlock packageBlock, List typeFilters, boolean writeEntries) throws IOException {
+ List entryGroupList = toSortedEntryGroups(packageBlock.listEntryGroup());
+ for(EntryGroup entryGroup : entryGroupList){
+ writeResources(entryGroup, writeEntries);
+ }
+ }
+ public abstract void writeResources(EntryGroup entryGroup, boolean writeEntries) throws IOException;
+ public abstract void writePackageNames(Collection packageBlocks) throws IOException;
+ public abstract void writeEntries(String name, List entryList) throws IOException;
+ public abstract void writeArray(String name, Object[] values) throws IOException;
+ public abstract void writeNameValue(String name, Object value) throws IOException;
+ public abstract void flush() throws IOException;
+ boolean contains(SpecTypePair specTypePair, List filterList){
+ if(filterList.size() == 0){
+ return true;
+ }
+ return filterList.contains(specTypePair.getTypeName());
+ }
+ public Writer getWriter() {
+ return writer;
+ }
+ @Override
+ public void close() throws IOException{
+ this.writer.close();
+ }
+
+ static String toString(Object obj){
+ if(obj != null){
+ return obj.toString();
+ }
+ return null;
+ }
+ static String getValueAsString(Entry entry){
+ ResValue resValue = entry.getResValue();
+ if(resValue == null){
+ return "";
+ }
+ return getValueAsString(resValue);
+ }
+ static String getValueAsString(Value value){
+ ValueType valueType = value.getValueType();
+ if(valueType == ValueType.STRING){
+ return value.getValueAsString();
+ }
+ String decoded = ValueCoder.decode(valueType, value.getData());
+ if(decoded != null){
+ return decoded;
+ }
+ if(valueType == ValueType.ATTRIBUTE){
+ return HexUtil.toHex8("?0x", value.getData());
+ }
+ if(valueType == ValueType.REFERENCE){
+ return HexUtil.toHex8("@0x", value.getData());
+ }
+ return HexUtil.toHex8("0x", value.getData());
+ }
+
+ static List toSortedEntryGroups(Collection entryGroups){
+ List results = new ArrayList<>(entryGroups);
+ sortEntryGroups(results);
+ return results;
+ }
+ static void sortEntryGroups(List entryGroups){
+ Comparator cmp = new Comparator() {
+ @Override
+ public int compare(EntryGroup entryGroup1, EntryGroup entryGroup2) {
+ long l1 = 0x00000000ffffffffL & entryGroup1.getResourceId();
+ long l2 = 0x00000000ffffffffL & entryGroup2.getResourceId();
+ return Long.compare(l1, l2);
+ }
+ };
+ entryGroups.sort(cmp);
+ }
+
+ static List sortEntries(Collection entryCollection) {
+ ArrayList results;
+ if(entryCollection instanceof ArrayList){
+ results = (ArrayList) entryCollection;
+ }else {
+ results = new ArrayList<>(entryCollection);
+ }
+ Comparator cmp = new Comparator() {
+ @Override
+ public int compare(Entry entry1, Entry entry2) {
+ return entry1.getResConfig().compareTo(entry2.getResConfig());
+ }
+ };
+ results.sort(cmp);
+ return results;
+ }
+
+ static final String TAG_RES_PACKAGES = "resource-packages";
+ static final String TAG_PUBLIC = "public";
+ static final String TAG_RESOURCES = "resources";
+ static final String NAME_RESOURCE = "resource";
+ static final String TAG_CONFIG = "config";
+ static final String TAG_VALUE = "value";
+ static final String TAG_BAG = "bag";
+ static final String TAG_ITEM = "item";
+ static final String NAME_QUALIFIERS = "qualifiers";
+
+}
diff --git a/src/main/java/com/reandroid/apkeditor/info/InfoWriterJson.java b/src/main/java/com/reandroid/apkeditor/info/InfoWriterJson.java
new file mode 100644
index 00000000..33160991
--- /dev/null
+++ b/src/main/java/com/reandroid/apkeditor/info/InfoWriterJson.java
@@ -0,0 +1,198 @@
+/*
+ * Copyright (C) 2022 github.com/REAndroid
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.reandroid.apkeditor.info;
+
+import com.reandroid.arsc.array.ResValueMapArray;
+import com.reandroid.arsc.chunk.PackageBlock;
+import com.reandroid.arsc.container.SpecTypePair;
+import com.reandroid.arsc.group.EntryGroup;
+import com.reandroid.arsc.value.Entry;
+import com.reandroid.arsc.value.ResTableMapEntry;
+import com.reandroid.arsc.value.ResValue;
+import com.reandroid.arsc.value.ResValueMap;
+import com.reandroid.json.JSONWriter;
+
+import java.io.IOException;
+import java.io.Writer;
+import java.util.Collection;
+import java.util.List;
+
+public class InfoWriterJson extends InfoWriter{
+ private final JSONWriter mJsonWriter;
+ public InfoWriterJson(Writer writer) {
+ super(writer);
+ JSONWriter jsonWriter = new JSONWriter(writer);
+ jsonWriter = jsonWriter.array();
+ this.mJsonWriter = jsonWriter;
+ }
+
+ @Override
+ public void writeResources(PackageBlock packageBlock, List typeFilters, boolean writeEntries) throws IOException {
+ packageBlock.sortTypes();
+ JSONWriter jsonWriter = mJsonWriter.object()
+ .key("id").value(packageBlock.getId())
+ .key("package").value(packageBlock.getName())
+ .key("types").array();
+
+ for(SpecTypePair specTypePair : packageBlock.listSpecTypePairs()){
+ if(!contains(specTypePair, typeFilters)){
+ continue;
+ }
+ writeResources(specTypePair, writeEntries);
+ }
+ jsonWriter.endArray()
+ .endObject();
+ }
+ public void writeResources(SpecTypePair specTypePair, boolean writeEntries) throws IOException {
+ JSONWriter jsonWriter = mJsonWriter.object()
+ .key("id").value(specTypePair.getId())
+ .key("type").value(specTypePair.getTypeName())
+ .key("entries").array();
+
+ List entryGroupList = toSortedEntryGroups(
+ specTypePair.createEntryGroups(true).values());
+
+ for(EntryGroup entryGroup : entryGroupList){
+ writeResources(entryGroup, writeEntries);
+ }
+ jsonWriter.endArray().endObject();
+ }
+ @Override
+ public void writeResources(EntryGroup entryGroup, boolean writeEntries) throws IOException {
+ JSONWriter jsonWriter = mJsonWriter.object()
+ .key("id").value(entryGroup.getResourceId())
+ .key("type").value(entryGroup.getTypeName())
+ .key("name").value(entryGroup.getSpecName());
+ if(writeEntries){
+ jsonWriter.key("configs");
+ writeEntries(sortEntries(entryGroup.listItems()));
+ }
+ jsonWriter.endObject();
+ }
+
+ public void writeEntries(List entryList) throws IOException {
+ JSONWriter jsonWriter = mJsonWriter.array();
+ for(Entry entry : entryList){
+ writeEntry(entry);
+ }
+ jsonWriter.endArray();
+ }
+ public void writeEntry(Entry entry) throws IOException {
+ if(entry.isComplex()){
+ writeBagEntry(entry);
+ }else {
+ writeResEntry(entry);
+ }
+ }
+ private void writeResEntry(Entry entry) throws IOException {
+ ResValue resValue = entry.getResValue();
+ if(resValue == null){
+ return;
+ }
+ mJsonWriter.object()
+ .key(NAME_QUALIFIERS).value(entry.getResConfig().getQualifiers())
+ .key("value").value(getValueAsString(resValue))
+ .endObject();
+ }
+ private void writeBagEntry(Entry entry) {
+ ResValueMapArray mapArray = entry.getResValueMapArray();
+ JSONWriter jsonWriter = mJsonWriter.object()
+ .key(NAME_QUALIFIERS).value(entry.getResConfig().getQualifiers())
+ .key("size").value(mapArray.childesCount())
+ .key("parent").value(((ResTableMapEntry)entry.getTableEntry()).getParentId())
+ .key(TAG_BAG).array();
+ for(ResValueMap resValueMap : mapArray.getChildes()){
+ writeValueMap(resValueMap);
+ }
+ jsonWriter.endArray()
+ .endObject();
+ }
+ private void writeValueMap(ResValueMap resValueMap){
+ mJsonWriter.object()
+ .key("name").value(resValueMap.decodeName())
+ .key("id").value(resValueMap.getNameResourceID())
+ .key("value").value(getValueAsString(resValueMap))
+ .endObject();
+ }
+ @Override
+ public void writePackageNames(Collection packageBlocks) throws IOException {
+ if(packageBlocks == null || packageBlocks.size() == 0){
+ return;
+ }
+ JSONWriter jsonWriter = mJsonWriter.object()
+ .key(TAG_RES_PACKAGES).array();
+
+ for(PackageBlock packageBlock : packageBlocks){
+ jsonWriter.object()
+ .key("id").value(packageBlock.getId())
+ .key("name").value(packageBlock.getName())
+ .endObject();
+ }
+ jsonWriter.endArray()
+ .endObject();
+ }
+ @Override
+ public void writeEntries(String name, List entryList) throws IOException {
+ if(entryList == null || entryList.size() == 0){
+ return;
+ }
+ Entry first = entryList.get(0);
+ JSONWriter jsonWriter = mJsonWriter.object()
+ .key("id").value(first.getResourceId())
+ .key("type").value(first.getTypeName())
+ .key("name").value(first.getName())
+ .key("entries")
+ .array();
+
+ for(Entry entry : entryList){
+ jsonWriter.object()
+ .key("config").value(entry.getResConfig().getQualifiers())
+ .key("value").value(getValueAsString(entry))
+ .endObject();
+ }
+ jsonWriter.endArray()
+ .endObject();
+ }
+ @Override
+ public void writeArray(String name, Object[] values) throws IOException {
+
+ JSONWriter jsonWriter = mJsonWriter.object()
+ .key(name)
+ .array();
+
+ for(Object value:values){
+ jsonWriter.value(value);
+ }
+
+ jsonWriter.endArray()
+ .endObject();
+ }
+ @Override
+ public void writeNameValue(String name, Object value) throws IOException {
+ mJsonWriter.object()
+ .key(name)
+ .value(value)
+ .endObject();
+ getWriter().flush();
+ }
+ @Override
+ public void flush() throws IOException {
+ Writer writer = getWriter();
+ mJsonWriter.endArray();
+ writer.write("\n");
+ writer.flush();
+ }
+}
diff --git a/src/main/java/com/reandroid/apkeditor/info/InfoWriterText.java b/src/main/java/com/reandroid/apkeditor/info/InfoWriterText.java
new file mode 100644
index 00000000..3cbbe186
--- /dev/null
+++ b/src/main/java/com/reandroid/apkeditor/info/InfoWriterText.java
@@ -0,0 +1,260 @@
+/*
+ * Copyright (C) 2022 github.com/REAndroid
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.reandroid.apkeditor.info;
+
+import com.reandroid.arsc.array.ResValueMapArray;
+import com.reandroid.arsc.chunk.PackageBlock;
+import com.reandroid.arsc.container.SpecTypePair;
+import com.reandroid.arsc.group.EntryGroup;
+import com.reandroid.arsc.util.HexUtil;
+import com.reandroid.arsc.value.Entry;
+import com.reandroid.arsc.value.ResTableMapEntry;
+import com.reandroid.arsc.value.ResValue;
+import com.reandroid.arsc.value.ResValueMap;
+
+import java.io.IOException;
+import java.io.Writer;
+import java.util.Collection;
+import java.util.List;
+
+public class InfoWriterText extends InfoWriter{
+ public InfoWriterText(Writer writer) {
+ super(writer);
+ }
+
+ @Override
+ public void writeResources(PackageBlock packageBlock, List typeFilters, boolean writeEntries) throws IOException {
+ Writer writer = getWriter();
+ writer.write("Package name=");
+ writer.write(packageBlock.getName());
+ writer.write(" id=");
+ writer.write(HexUtil.toHex2((byte) packageBlock.getId()));
+ writer.write("\n");
+ packageBlock.sortTypes();
+
+ for(SpecTypePair specTypePair : packageBlock.listSpecTypePairs()){
+ if(!contains(specTypePair, typeFilters)){
+ continue;
+ }
+ writeResources(specTypePair, writeEntries);
+ }
+ }
+ public void writeResources(SpecTypePair specTypePair, boolean writeEntries) throws IOException {
+ Writer writer = getWriter();
+ writer.write(" type ");
+ writer.write(specTypePair.getTypeName());
+ writer.write(" id=");
+ writer.write(Integer.toString(specTypePair.getId()));
+ writer.write(" entryCount=");
+ writer.write(Integer.toString(specTypePair.getHighestEntryCount()));
+ writer.write("\n");
+ List entryGroupList = toSortedEntryGroups(
+ specTypePair.createEntryGroups(true).values());
+
+ for(EntryGroup entryGroup : entryGroupList){
+ writeResources(entryGroup, writeEntries);
+ }
+ }
+ @Override
+ public void writeResources(EntryGroup entryGroup, boolean writeEntries) throws IOException {
+ Writer writer = getWriter();
+ writer.write(" ");
+ writer.write(NAME_RESOURCE);
+ writer.write(" ");
+ writer.write(HexUtil.toHex8(entryGroup.getResourceId()));
+ writer.write(" ");
+ writer.write(entryGroup.getTypeName());
+ writer.write("/");
+ writer.write(entryGroup.getSpecName());
+ writer.write("\n");
+ if(writeEntries){
+ writeEntries(sortEntries(entryGroup.listItems()));
+ }
+ writer.flush();
+ }
+
+ public void writeEntries(List entryList) throws IOException {
+ for(Entry entry : entryList){
+ writeEntry(entry);
+ }
+ }
+ public void writeEntry(Entry entry) throws IOException {
+ Writer writer = getWriter();
+ writer.write(" (");
+ writer.write(entry.getResConfig().getQualifiers());
+ writer.write(") ");
+ //write file
+ if(entry.isComplex()){
+ writeBagEntry(entry);
+ }else {
+ writeResEntry(entry);
+ }
+ writer.flush();
+ }
+ private void writeResEntry(Entry entry) throws IOException {
+ ResValue resValue = entry.getResValue();
+ if(resValue == null){
+ return;
+ }
+ Writer writer = getWriter();
+ writer.write(getValueAsString(resValue));
+ writer.write("\n");
+ }
+ private void writeBagEntry(Entry entry) throws IOException {
+ Writer writer = getWriter();
+ ResValueMapArray mapArray = entry.getResValueMapArray();
+ writer.write(" size=");
+ writer.write(Integer.toString(mapArray.childesCount()));
+ writer.write(" parent=");
+ writer.write(HexUtil.toHex8(((ResTableMapEntry)entry.getTableEntry()).getParentId()));
+ writer.write("\n");
+ for(ResValueMap resValueMap : mapArray.getChildes()){
+ writeValueMap(resValueMap);
+ }
+ }
+ private void writeValueMap(ResValueMap resValueMap) throws IOException {
+ Writer writer = getWriter();
+ writer.write(" ");
+ String name = resValueMap.decodeName();
+ if(name != null){
+ writer.write(name);
+ writer.write("(");
+ }
+ writer.write(HexUtil.toHex8(resValueMap.getNameResourceID()));
+ if(name != null){
+ writer.write(")");
+ }
+ writer.write("=");
+ writer.write(getValueAsString(resValueMap));
+ writer.write("\n");
+ }
+ @Override
+ public void writePackageNames(Collection packageBlocks) throws IOException {
+ if(packageBlocks == null || packageBlocks.size() == 0){
+ return;
+ }
+ Writer writer = getWriter();
+ writer.write(TAG_RES_PACKAGES);
+ writer.write(" [ count ");
+ writer.write(Integer.toString(packageBlocks.size()));
+ writer.write("]");
+ writer.write("\n");
+ for(PackageBlock packageBlock : packageBlocks){
+ writer.write(ARRAY_TAB);
+ writer.write(HexUtil.toHex2((byte) packageBlock.getId()));
+ writer.write(" \"");
+ writer.write(packageBlock.getName());
+ writer.write("\"");
+ writer.write("\n");
+ }
+ }
+ @Override
+ public void writeEntries(String name, List entryList) throws IOException {
+ if(entryList == null || entryList.size() == 0){
+ return;
+ }
+ Entry first = entryList.get(0);
+ Writer writer = getWriter();
+ writer.write(name);
+ writer.write(" [ configs = ");
+ writer.write(Integer.toString(entryList.size()));
+ writer.write(", id=");
+ writer.write(HexUtil.toHex8(first.getResourceId()));
+ writer.write(", type=");
+ writer.write(first.getTypeName());
+ writer.write(", name=");
+ writer.write(first.getName());
+ writer.write(" ]");
+ writer.write("\n");
+ int index = 0;
+ for(Entry entry : entryList){
+ index++;
+ String config = entry.getResConfig().getQualifiers();
+ if(config.length() == 0){
+ config = "default";
+ }
+ writer.write(ARRAY_TAB);
+ writer.write(config);
+ writer.write(" \"");
+ String text = getValueAsString(entry);
+ writer.write(text);
+ writer.write("\"\n");
+ if((index % 3) == 0){
+ writer.flush();
+ }
+ }
+ }
+
+ @Override
+ public void writeArray(String name, Object[] values) throws IOException {
+ if(values == null){
+ return;
+ }
+ Writer writer = getWriter();
+ writer.write(name);
+ writer.write(" [ count ");
+ writer.write(Integer.toString(values.length));
+ writer.write("]");
+ writer.write("\n");
+ String format = "%0" + getDecimalPlaces(values.length) + "d) ";
+ int index = 0;
+ for(Object value : values){
+ index ++;
+ writer.write(ARRAY_TAB);
+ writer.write(String.format(format, index));
+ String text = toString(value);
+ if(text == null){
+ text = "null";
+ }
+ writer.write(text);
+ writer.write("\n");
+ if((index % 3) == 0){
+ writer.flush();
+ }
+ }
+ }
+ @Override
+ public void writeNameValue(String name, Object value) throws IOException {
+ String text = toString(value);
+ if(text == null){
+ return;
+ }
+ Writer writer = getWriter();
+ writer.write(name);
+ writer.write("=\"");
+ writer.write(text);
+ writer.write("\"");
+ writer.write("\n");
+ writer.flush();
+ }
+
+ @Override
+ public void flush() throws IOException {
+ Writer writer = getWriter();
+ writer.flush();
+ }
+
+ private static int getDecimalPlaces(int max){
+ int i = 0;
+ while (max != 0){
+ i++;
+ max = max / 10;
+ }
+ return i;
+ }
+
+ private static final String ARRAY_TAB = " ";
+}
diff --git a/src/main/java/com/reandroid/apkeditor/info/InfoWriterXml.java b/src/main/java/com/reandroid/apkeditor/info/InfoWriterXml.java
new file mode 100644
index 00000000..dde8eac1
--- /dev/null
+++ b/src/main/java/com/reandroid/apkeditor/info/InfoWriterXml.java
@@ -0,0 +1,325 @@
+/*
+ * Copyright (C) 2022 github.com/REAndroid
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.reandroid.apkeditor.info;
+
+import com.android.org.kxml2.io.KXmlSerializer;
+import com.reandroid.arsc.array.ResValueMapArray;
+import com.reandroid.arsc.chunk.PackageBlock;
+import com.reandroid.arsc.container.SpecTypePair;
+import com.reandroid.arsc.group.EntryGroup;
+import com.reandroid.arsc.util.HexUtil;
+import com.reandroid.arsc.value.Entry;
+import com.reandroid.arsc.value.ResTableMapEntry;
+import com.reandroid.arsc.value.ResValue;
+import com.reandroid.arsc.value.ResValueMap;
+
+import java.io.IOException;
+import java.io.Writer;
+import java.util.Collection;
+import java.util.List;
+
+public class InfoWriterXml extends InfoWriter{
+ private KXmlSerializer mSerializer;
+ private String mRootTag = TAG_INFO;
+ private int mIndent;
+ public InfoWriterXml(Writer writer) {
+ super(writer);
+ }
+ @Override
+ public void writeResources(PackageBlock packageBlock, List typeFilters, boolean writeEntries) throws IOException {
+ KXmlSerializer serializer = getSerializer();
+ int indent = mIndent + 2;
+ mIndent = indent;
+ writeIndent(serializer, indent);
+ serializer.startTag(null, "package");
+ serializer.attribute(null, "id", HexUtil.toHex2((byte) packageBlock.getId()));
+ serializer.attribute(null, "name", packageBlock.getName());
+ packageBlock.sortTypes();
+
+ for(SpecTypePair specTypePair : packageBlock.listSpecTypePairs()){
+ if(!contains(specTypePair, typeFilters)){
+ continue;
+ }
+ writeResources(specTypePair, writeEntries);
+ }
+
+ writeIndent(serializer, indent);
+ indent = indent - 2;
+ mIndent = indent;
+ serializer.endTag(null, "package");
+ }
+ public void writeResources(SpecTypePair specTypePair, boolean writeEntries) throws IOException {
+ KXmlSerializer serializer = getSerializer();
+ int indent = mIndent + 2;
+ mIndent = indent;
+ writeIndent(serializer, indent);
+ serializer.startTag(null, "type");
+
+ serializer.attribute(null, "name",
+ specTypePair.getTypeName());
+ serializer.attribute(null, "id",
+ Integer.toString(specTypePair.getId()));
+ serializer.attribute(null, "entryCount",
+ Integer.toString(specTypePair.getHighestEntryCount()));
+ List entryGroupList = toSortedEntryGroups(
+ specTypePair.createEntryGroups(true).values());
+
+ for(EntryGroup entryGroup : entryGroupList){
+ writeResources(entryGroup, writeEntries);
+ }
+
+ writeIndent(serializer, indent);
+ indent = indent - 2;
+ mIndent = indent;
+ serializer.endTag(null, "type");
+ serializer.flush();
+ }
+ @Override
+ public void writeResources(EntryGroup entryGroup, boolean writeEntries) throws IOException {
+ KXmlSerializer serializer = getSerializer();
+ int indent = mIndent + 2;
+ mIndent = indent;
+ writeIndent(serializer, indent);
+ serializer.startTag(null, NAME_RESOURCE);
+ serializer.attribute(null, "id", HexUtil.toHex8(entryGroup.getResourceId()));
+ serializer.attribute(null, "type", entryGroup.getTypeName());
+ serializer.attribute(null, "name", entryGroup.getSpecName());
+
+ if(writeEntries){
+
+ writeEntries(sortEntries(entryGroup.listItems()));
+
+ writeIndent(serializer, indent);
+ }
+
+ indent = indent - 2;
+ mIndent = indent;
+
+ serializer.endTag(null, NAME_RESOURCE);
+ serializer.flush();
+ }
+ public void writeEntries(List entryList) throws IOException {
+ for(Entry entry : entryList){
+ writeEntry(entry);
+ }
+ }
+ public void writeEntry(Entry entry) throws IOException {
+ KXmlSerializer serializer = getSerializer();
+ int indent = mIndent + 2;
+ mIndent = indent;
+ writeIndent(serializer, indent);
+ serializer.startTag(null, TAG_CONFIG);
+ serializer.attribute(null, NAME_QUALIFIERS,
+ entry.getResConfig().getQualifiers());
+ if(entry.isComplex()){
+ writeBagEntry(entry);
+ }else {
+ writeResEntry(entry);
+ }
+ writeIndent(serializer, indent);
+ serializer.endTag(null, TAG_CONFIG);
+ indent = indent - 2;
+ mIndent = indent;
+ }
+ private void writeResEntry(Entry entry) throws IOException {
+ ResValue resValue = entry.getResValue();
+ if(resValue == null){
+ return;
+ }
+ KXmlSerializer serializer = getSerializer();
+ int indent = mIndent + 2;
+ mIndent = indent;
+ writeIndent(serializer, indent);
+ serializer.startTag(null, TAG_VALUE);
+ serializer.attribute(null, "type", resValue.getValueType().name());
+ serializer.text(getValueAsString(resValue));
+ serializer.endTag(null, TAG_VALUE);
+ indent = indent - 2;
+ mIndent = indent;
+ }
+ private void writeBagEntry(Entry entry) throws IOException {
+ KXmlSerializer serializer = getSerializer();
+ int indent = mIndent + 2;
+ mIndent = indent;
+ writeIndent(serializer, indent);
+ serializer.startTag(null, TAG_BAG);
+ serializer.attribute(null, "parent",
+ HexUtil.toHex8(((ResTableMapEntry)entry.getTableEntry()).getParentId()));
+ ResValueMapArray mapArray = entry.getResValueMapArray();
+ for(ResValueMap resValueMap : mapArray.getChildes()){
+ writeValueMap(resValueMap);
+ }
+ writeIndent(serializer, indent);
+ serializer.endTag(null, TAG_BAG);
+ indent = indent - 2;
+ mIndent = indent;
+ }
+ private void writeValueMap(ResValueMap resValueMap) throws IOException {
+ KXmlSerializer serializer = getSerializer();
+ int indent = mIndent + 2;
+ mIndent = indent;
+ writeIndent(serializer, indent);
+ serializer.startTag(null, TAG_VALUE);
+ serializer.attribute(null, "name",
+ HexUtil.toHex8(resValueMap.getNameResourceID()));
+ serializer.attribute(null, "type", resValueMap.getValueType().name());
+ serializer.text(getValueAsString(resValueMap));
+ serializer.endTag(null, TAG_VALUE);
+ indent = indent - 2;
+ mIndent = indent;
+ }
+ @Override
+ public void writePackageNames(Collection packageBlocks) throws IOException {
+ if(packageBlocks == null || packageBlocks.size() == 0){
+ return;
+ }
+ int level = INDENT;
+ KXmlSerializer serializer = getSerializer();
+ writeIndent(serializer, level);
+ serializer.startTag(null, TAG_RES_PACKAGES);
+ serializer.attribute(null, "count", Integer.toString(packageBlocks.size()));
+ level = level + 2;
+ for(PackageBlock packageBlock : packageBlocks){
+ writeIndent(serializer, level);
+ serializer.startTag(null, "package");
+ serializer.attribute(null, "id",
+ HexUtil.toHex2((byte) packageBlock.getId()));
+ serializer.attribute(null, "name",
+ packageBlock.getName());
+ serializer.endTag(null, "package");
+ }
+ level = level - 2;
+ writeIndent(serializer, level);
+ serializer.endTag(null, TAG_RES_PACKAGES);
+ serializer.flush();
+ }
+ @Override
+ public void writeEntries(String name, List entryList) throws IOException {
+ if(entryList == null || entryList.size() == 0){
+ return;
+ }
+ Entry first = entryList.get(0);
+ int level = INDENT;
+ KXmlSerializer serializer = getSerializer();
+ writeIndent(serializer, level);
+ serializer.startTag(null, name);
+ serializer.attribute(null, "count", Integer.toString(entryList.size()));
+ serializer.attribute(null, "id", HexUtil.toHex8(first.getResourceId()));
+ serializer.attribute(null, "type", first.getTypeName());
+ serializer.attribute(null, "name", first.getName());
+ level = level + 2;
+ for(Entry entry : entryList){
+ String config = entry.getResConfig().getQualifiers();
+ if(config.length() == 0){
+ config = "default";
+ }
+ String text = getValueAsString(entry);
+ writeIndent(serializer, level);
+ serializer.startTag(null, "item");
+ serializer.attribute(null, "config", config);
+ serializer.text(text);
+ serializer.endTag(null, "item");
+ }
+ level = level - 2;
+ writeIndent(serializer, level);
+ serializer.endTag(null, name);
+ serializer.flush();
+ }
+
+ @Override
+ public void writeArray(String name, Object[] values) throws IOException {
+ if(values == null){
+ return;
+ }
+ int level = INDENT;
+ KXmlSerializer serializer = getSerializer();
+ writeIndent(serializer, level);
+ serializer.startTag(null, name);
+ serializer.attribute(null, "count", Integer.toString(values.length));
+ level = level + 2;
+ for(Object value : values){
+ String text = toString(value);
+ if(text == null){
+ text = "";
+ }
+ writeIndent(serializer, level);
+ serializer.startTag(null, "item");
+ serializer.text(text);
+ serializer.endTag(null, "item");
+ }
+ level = level - 2;
+ writeIndent(serializer, level);
+ serializer.endTag(null, name);
+ serializer.flush();
+ }
+
+ @Override
+ public void writeNameValue(String name, Object value) throws IOException {
+ String text = toString(value);
+ if(text == null){
+ return;
+ }
+ KXmlSerializer serializer = getSerializer();
+ writeIndent(serializer, INDENT);
+ serializer.startTag(null, name);
+ serializer.text(text);
+ serializer.endTag(null, name);
+ serializer.flush();
+ }
+
+ @Override
+ public void flush() throws IOException {
+ KXmlSerializer serializer = this.mSerializer;
+ if(serializer != null){
+ writeIndent(serializer, 0);
+ serializer.endTag(null, mRootTag);
+ serializer.endDocument();
+ writeIndent(serializer, 0);
+ serializer.flush();
+ }
+ }
+ private void writeIndent(KXmlSerializer serializer, int level) throws IOException {
+ StringBuilder builder = new StringBuilder();
+ builder.append('\n');
+ for(int i = 0; i < level; i++){
+ builder.append(' ');
+ }
+ serializer.text(builder.toString());
+ }
+ private void setRootTag(String tag){
+ KXmlSerializer serializer = this.mSerializer;
+ if(serializer != null){
+ return;
+ }
+ mRootTag = tag;
+ }
+ private KXmlSerializer getSerializer() throws IOException {
+ KXmlSerializer serializer = this.mSerializer;
+ if(serializer != null){
+ return serializer;
+ }
+ serializer = new KXmlSerializer();
+ serializer.setOutput(getWriter());
+ serializer.startDocument("utf-8", null);
+ writeIndent(serializer, 0);
+ serializer.startTag(null, mRootTag);
+ mSerializer = serializer;
+ return serializer;
+ }
+
+ private static final int INDENT = 3;
+ private static final String TAG_INFO = "info";
+}
diff --git a/src/main/java/com/reandroid/apkeditor/merge/Merger.java b/src/main/java/com/reandroid/apkeditor/merge/Merger.java
index 7d245630..4cc386f7 100644
--- a/src/main/java/com/reandroid/apkeditor/merge/Merger.java
+++ b/src/main/java/com/reandroid/apkeditor/merge/Merger.java
@@ -1,48 +1,48 @@
- /*
- * Copyright (C) 2022 github.com/REAndroid
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
+/*
+ * Copyright (C) 2022 github.com/REAndroid
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
package com.reandroid.apkeditor.merge;
- import com.reandroid.apkeditor.BaseCommand;
- import com.reandroid.apkeditor.Util;
- import com.reandroid.apkeditor.common.AndroidManifestHelper;
- import com.reandroid.archive.APKArchive;
- import com.reandroid.archive.WriteProgress;
- import com.reandroid.archive2.Archive;
- import com.reandroid.arsc.value.ResTableEntry;
- import com.reandroid.arsc.value.ResValue;
- import com.reandroid.commons.command.ARGException;
- import com.reandroid.commons.utils.log.Logger;
- import com.reandroid.apk.APKLogger;
- import com.reandroid.apk.ApkBundle;
- import com.reandroid.apk.ApkModule;
- import com.reandroid.arsc.chunk.TableBlock;
- import com.reandroid.arsc.chunk.xml.AndroidManifestBlock;
- import com.reandroid.arsc.chunk.xml.ResXmlAttribute;
- import com.reandroid.arsc.chunk.xml.ResXmlElement;
- import com.reandroid.arsc.group.EntryGroup;
- import com.reandroid.arsc.value.Entry;
- import com.reandroid.arsc.value.ValueType;
+import com.reandroid.apkeditor.BaseCommand;
+import com.reandroid.apkeditor.Util;
+import com.reandroid.apkeditor.common.AndroidManifestHelper;
+import com.reandroid.archive.APKArchive;
+import com.reandroid.archive.WriteProgress;
+import com.reandroid.archive2.Archive;
+import com.reandroid.arsc.container.SpecTypePair;
+import com.reandroid.arsc.value.ResValue;
+import com.reandroid.commons.command.ARGException;
+import com.reandroid.commons.utils.log.Logger;
+import com.reandroid.apk.APKLogger;
+import com.reandroid.apk.ApkBundle;
+import com.reandroid.apk.ApkModule;
+import com.reandroid.arsc.chunk.TableBlock;
+import com.reandroid.arsc.chunk.xml.AndroidManifestBlock;
+import com.reandroid.arsc.chunk.xml.ResXmlAttribute;
+import com.reandroid.arsc.chunk.xml.ResXmlElement;
+import com.reandroid.arsc.group.EntryGroup;
+import com.reandroid.arsc.value.Entry;
+import com.reandroid.arsc.value.ValueType;
- import java.io.File;
- import java.io.IOException;
- import java.util.List;
- import java.util.Objects;
- import java.util.zip.ZipEntry;
+import java.io.File;
+import java.io.IOException;
+import java.util.List;
+import java.util.Objects;
+import java.util.zip.ZipEntry;
- public class Merger extends BaseCommand implements WriteProgress {
+public class Merger extends BaseCommand implements WriteProgress {
private final MergerOptions options;
private APKLogger mApkLogger;
public Merger(MergerOptions options){
@@ -79,13 +79,19 @@ public void run() throws IOException {
}
if(options.cleanMeta){
log("Clearing META-INF ...");
- removeSignature(mergedModule);
- mergedModule.setApkSignatureBlock(null);
- }
- if(mergedModule.hasAndroidManifestBlock()){
- sanitizeManifest(mergedModule);
+ clearMeta(mergedModule);
}
+ sanitizeManifest(mergedModule);
Util.addApkEditorInfo(mergedModule, getClass().getSimpleName());
+ String message = mergedModule.refreshTable();
+ if(message != null){
+ log(message);
+ }
+ message = mergedModule.refreshManifest();
+ if(message != null){
+ log(message);
+ }
+
log("Writing apk ...");
mergedModule.writeApk(options.outputFile, this);
if(extracted){
@@ -119,9 +125,11 @@ private File toTmpDir(File file){
}
return new File(dir, name);
}
- private void sanitizeManifest(ApkModule apkModule) throws IOException {
- AndroidManifestBlock manifest=apkModule.getAndroidManifestBlock();
-
+ private void sanitizeManifest(ApkModule apkModule) {
+ if(!apkModule.hasAndroidManifestBlock()){
+ return;
+ }
+ AndroidManifestBlock manifest = apkModule.getAndroidManifestBlock();
log("Sanitizing manifest ...");
boolean removed = AndroidManifestHelper.removeApplicationAttribute(manifest,
AndroidManifestBlock.ID_extractNativeLibs);
@@ -145,9 +153,9 @@ private void sanitizeManifest(ApkModule apkModule) throws IOException {
}
manifest.refresh();
}
- private boolean removeSplitsTableEntry(ResXmlElement metaElement, ApkModule apkModule) throws IOException {
+ private boolean removeSplitsTableEntry(ResXmlElement metaElement, ApkModule apkModule) {
ResXmlAttribute nameAttribute = metaElement.searchAttributeByResourceId(AndroidManifestBlock.ID_name);
- if(nameAttribute.getValueType()!= ValueType.STRING){
+ if(nameAttribute == null){
return false;
}
if(!"com.android.vending.splits".equals(nameAttribute.getValueAsString())){
@@ -159,30 +167,39 @@ private boolean removeSplitsTableEntry(ResXmlElement metaElement, ApkModule apkM
valueAttribute=metaElement.searchAttributeByResourceId(
AndroidManifestBlock.ID_resource);
}
- if(valueAttribute==null || valueAttribute.getValueType()!=ValueType.REFERENCE){
+ if(valueAttribute == null
+ || valueAttribute.getValueType() != ValueType.REFERENCE){
+ return false;
+ }
+ if(!apkModule.hasTableBlock()){
return false;
}
- TableBlock tableBlock=apkModule.getTableBlock();
+ TableBlock tableBlock = apkModule.getTableBlock();
EntryGroup entryGroup = tableBlock.search(valueAttribute.getData());
- if(entryGroup==null){
+ if(entryGroup == null){
return false;
}
- APKArchive apkArchive=apkModule.getApkArchive();
+ APKArchive apkArchive = apkModule.getApkArchive();
List entryList = entryGroup.listItems();
- for(Entry entryBlock:entryList){
- if(entryBlock==null){
+ for(Entry entry : entryList){
+ if(entry == null){
+ continue;
+ }
+ ResValue resValue = entry.getResValue();
+ if(resValue == null){
continue;
}
- ResValue resValue = ((ResTableEntry)entryBlock.getTableEntry()).getValue();
String path = resValue.getValueAsString();
log("Removed from table: "+path);
//Remove file entry
apkArchive.remove(path);
// It's not safe to destroy entry, resource id might be used in dex code.
- // Better replace it with boolean value
- entryBlock.setNull(true);
+ // Better replace it with boolean value.
+ entry.setNull(true);
+ SpecTypePair specTypePair = entry.getTypeBlock()
+ .getParentSpecTypePair();
+ specTypePair.removeNullEntries(entry.getId());
}
- tableBlock.refresh();
return true;
}
@Override
@@ -264,5 +281,5 @@ public static boolean isCommand(String command){
}
public static final String ARG_SHORT="m";
public static final String ARG_LONG="merge";
- public static final String DESCRIPTION="Merges split apk files from directory or XAPK, APKM, APKS ...";
+ public static final String DESCRIPTION="Merges split apk files from directory or compressed apk files like XAPK, APKM, APKS ...";
}
diff --git a/src/main/java/com/reandroid/apkeditor/merge/MergerOptions.java b/src/main/java/com/reandroid/apkeditor/merge/MergerOptions.java
index 58d7d9ef..ac96dbc8 100644
--- a/src/main/java/com/reandroid/apkeditor/merge/MergerOptions.java
+++ b/src/main/java/com/reandroid/apkeditor/merge/MergerOptions.java
@@ -108,13 +108,13 @@ public static String getHelp(){
new String[]{ARG_output, ARG_DESC_output},
new String[]{ARG_resDir, ARG_DESC_resDir}
};
- StringHelper.printTwoColumns(builder, " ", 75, table);
+ StringHelper.printTwoColumns(builder, " ", Options.PRINT_WIDTH, table);
builder.append("\nFlags:\n");
table=new String[][]{
new String[]{ARG_force, ARG_DESC_force},
new String[]{ARG_cleanMeta, ARG_DESC_cleanMeta}
};
- StringHelper.printTwoColumns(builder, " ", 75, table);
+ StringHelper.printTwoColumns(builder, " ", Options.PRINT_WIDTH, table);
String jar = APKEditor.getJarName();
builder.append("\n\nExample-1:");
builder.append("\n java -jar ").append(jar).append(" ").append(Merger.ARG_SHORT).append(" ")
diff --git a/src/main/java/com/reandroid/apkeditor/protect/ProtectorOptions.java b/src/main/java/com/reandroid/apkeditor/protect/ProtectorOptions.java
index 36935cc9..760e6400 100644
--- a/src/main/java/com/reandroid/apkeditor/protect/ProtectorOptions.java
+++ b/src/main/java/com/reandroid/apkeditor/protect/ProtectorOptions.java
@@ -85,13 +85,13 @@ public static String getHelp(){
new String[]{ARG_input, ARG_DESC_input},
new String[]{ARG_output, ARG_DESC_output}
};
- StringHelper.printTwoColumns(builder, " ", 75, table);
+ StringHelper.printTwoColumns(builder, " ", Options.PRINT_WIDTH, table);
builder.append("\nFlags:\n");
table=new String[][]{
new String[]{ARG_force, ARG_DESC_force},
new String[]{ARG_skipManifest, ARG_DESC_skipManifest}
};
- StringHelper.printTwoColumns(builder, " ", 75, table);
+ StringHelper.printTwoColumns(builder, " ", Options.PRINT_WIDTH, table);
String jar = APKEditor.getJarName();
builder.append("\n\nExample-1:");
builder.append("\n java -jar ").append(jar).append(" ").append(Protector.ARG_SHORT).append(" ")
diff --git a/src/main/java/com/reandroid/apkeditor/refactor/Refactor.java b/src/main/java/com/reandroid/apkeditor/refactor/Refactor.java
index ecf77365..537aa165 100644
--- a/src/main/java/com/reandroid/apkeditor/refactor/Refactor.java
+++ b/src/main/java/com/reandroid/apkeditor/refactor/Refactor.java
@@ -1,147 +1,151 @@
- /*
- * Copyright (C) 2022 github.com/REAndroid
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
+/*
+ * Copyright (C) 2022 github.com/REAndroid
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
package com.reandroid.apkeditor.refactor;
- import com.reandroid.apkeditor.BaseCommand;
- import com.reandroid.apkeditor.Util;
- import com.reandroid.archive.WriteProgress;
- import com.reandroid.commons.command.ARGException;
- import com.reandroid.commons.utils.log.Logger;
- import com.reandroid.apk.APKLogger;
- import com.reandroid.apk.ApkModule;
+import com.reandroid.apkeditor.BaseCommand;
+import com.reandroid.apkeditor.Util;
+import com.reandroid.archive.WriteProgress;
+import com.reandroid.commons.command.ARGException;
+import com.reandroid.commons.utils.log.Logger;
+import com.reandroid.apk.APKLogger;
+import com.reandroid.apk.ApkModule;
- import java.io.File;
- import java.io.IOException;
+import java.io.File;
+import java.io.IOException;
- public class Refactor extends BaseCommand implements WriteProgress {
- private final RefactorOptions options;
- private APKLogger mApkLogger;
- public Refactor(RefactorOptions options){
- this.options=options;
- }
- public void run() throws IOException {
- log("Loading apk: "+options.inputFile);
- ApkModule module=ApkModule.loadApkFile(options.inputFile);
- module.setAPKLogger(getAPKLogger());
- if(!module.hasTableBlock()){
- throw new IOException("Don't have resources.arsc");
- }
- String protect = Util.isProtected(module);
- if(protect!=null){
- log(options.inputFile.getAbsolutePath());
- log(protect);
- return;
- }
- if(options.fixTypeNames){
- TypeNameRefactor typeNameRefactor=new TypeNameRefactor(module);
- typeNameRefactor.setApkLogger(getAPKLogger());
- typeNameRefactor.refactor();
- }
- log("Auto refactoring ...");
- AutoRefactor autoRefactor=new AutoRefactor(module);
- int autoRenameCount=autoRefactor.refactor();
- log("Auto renamed entries: "+autoRenameCount);
- StringValueNameGenerator generator = new StringValueNameGenerator(module.getTableBlock());
- generator.refactor();
- if(options.publicXml!=null){
- log("Renaming from: "+options.publicXml);
- PublicXmlRefactor publicXmlRefactor =
- new PublicXmlRefactor(module, options.publicXml);
- int pubXmlRenameCount = publicXmlRefactor.refactor();
- log("Renamed from public.xml entries: "+pubXmlRenameCount);
- }
- if(options.cleanMeta){
- log("Clearing META-INF ...");
- removeSignature(module);
- }
- Util.addApkEditorInfo(module, getClass().getSimpleName());
- log("Writing apk ...");
- module.writeApk(options.outputFile, this);
- log("Saved to: "+options.outputFile);
- log("Done");
- }
- @Override
- public void onCompressFile(String path, int method, long length) {
- StringBuilder builder=new StringBuilder();
- builder.append("Writing: ");
- if(path.length()>30){
- path=path.substring(path.length()-30);
- }
- builder.append(path);
- logSameLine(builder.toString());
- }
- private APKLogger getAPKLogger(){
- if(mApkLogger!=null){
- return mApkLogger;
- }
- mApkLogger = new APKLogger() {
- @Override
- public void logMessage(String msg) {
- Logger.i(getLogTag()+msg);
- }
- @Override
- public void logError(String msg, Throwable tr) {
- Logger.e(getLogTag()+msg, tr);
- }
- @Override
- public void logVerbose(String msg) {
- if(msg.length()>30){
- msg=msg.substring(msg.length()-30);
- }
- Logger.sameLine(getLogTag()+msg);
- }
- };
- return mApkLogger;
- }
- public static void execute(String[] args) throws ARGException, IOException {
- if(Util.isHelp(args)){
- throw new ARGException(RefactorOptions.getHelp());
- }
- RefactorOptions option=new RefactorOptions();
- option.parse(args);
- File outFile=option.outputFile;
- Util.deleteEmptyDirectories(outFile);
- if(outFile.exists()){
- if(!option.force){
- throw new ARGException("Path already exists: "+outFile);
- }
- log("Deleting: "+outFile);
- Util.deleteDir(outFile);
- }
- log("Refactoring ...\n"+option);
- Refactor refactor=new Refactor(option);
- refactor.run();
- }
- private static void logSameLine(String msg){
- Logger.sameLine(getLogTag()+msg);
- }
- private static void log(String msg){
- Logger.i(getLogTag()+msg);
- }
- private static String getLogTag(){
- return "[REFACTOR] ";
- }
- public static boolean isCommand(String command){
- if(Util.isEmpty(command)){
- return false;
- }
- command=command.toLowerCase().trim();
- return command.equals(ARG_SHORT) || command.equals(ARG_LONG);
- }
- public static final String ARG_SHORT="x";
- public static final String ARG_LONG="refactor";
- public static final String DESCRIPTION="Refactors obfuscated resource names";
+public class Refactor extends BaseCommand implements WriteProgress {
+ private final RefactorOptions options;
+ private APKLogger mApkLogger;
+ public Refactor(RefactorOptions options){
+ this.options=options;
+ }
+ public void run() throws IOException {
+ log("Loading apk: "+options.inputFile);
+ ApkModule module=ApkModule.loadApkFile(options.inputFile);
+ module.setAPKLogger(getAPKLogger());
+ if(!module.hasTableBlock()){
+ throw new IOException("Don't have resources.arsc");
+ }
+ String protect = Util.isProtected(module);
+ if(protect!=null){
+ log(options.inputFile.getAbsolutePath());
+ log(protect);
+ return;
+ }
+ if(options.fixTypeNames){
+ TypeNameRefactor typeNameRefactor=new TypeNameRefactor(module);
+ typeNameRefactor.setApkLogger(getAPKLogger());
+ typeNameRefactor.refactor();
+ }
+ log("Auto refactoring ...");
+ AutoRefactor autoRefactor=new AutoRefactor(module);
+ int autoRenameCount=autoRefactor.refactor();
+ log("Auto renamed entries: "+autoRenameCount);
+ StringValueNameGenerator generator = new StringValueNameGenerator(module.getTableBlock());
+ generator.refactor();
+ if(options.publicXml!=null){
+ log("Renaming from: "+options.publicXml);
+ PublicXmlRefactor publicXmlRefactor =
+ new PublicXmlRefactor(module, options.publicXml);
+ int pubXmlRenameCount = publicXmlRefactor.refactor();
+ log("Renamed from public.xml entries: "+pubXmlRenameCount);
+ }
+ if(options.cleanMeta){
+ log("Clearing META-INF ...");
+ clearMeta(module);
+ }
+ Util.addApkEditorInfo(module, getClass().getSimpleName());
+ String message = module.refreshTable();
+ if(message != null){
+ log(message);
+ }
+ log("Writing apk ...");
+ module.writeApk(options.outputFile, this);
+ log("Saved to: "+options.outputFile);
+ log("Done");
+ }
+ @Override
+ public void onCompressFile(String path, int method, long length) {
+ StringBuilder builder=new StringBuilder();
+ builder.append("Writing: ");
+ if(path.length()>30){
+ path=path.substring(path.length()-30);
+ }
+ builder.append(path);
+ logSameLine(builder.toString());
+ }
+ private APKLogger getAPKLogger(){
+ if(mApkLogger!=null){
+ return mApkLogger;
+ }
+ mApkLogger = new APKLogger() {
+ @Override
+ public void logMessage(String msg) {
+ Logger.i(getLogTag()+msg);
+ }
+ @Override
+ public void logError(String msg, Throwable tr) {
+ Logger.e(getLogTag()+msg, tr);
+ }
+ @Override
+ public void logVerbose(String msg) {
+ if(msg.length()>30){
+ msg=msg.substring(msg.length()-30);
+ }
+ Logger.sameLine(getLogTag()+msg);
+ }
+ };
+ return mApkLogger;
+ }
+ public static void execute(String[] args) throws ARGException, IOException {
+ if(Util.isHelp(args)){
+ throw new ARGException(RefactorOptions.getHelp());
+ }
+ RefactorOptions option=new RefactorOptions();
+ option.parse(args);
+ File outFile=option.outputFile;
+ Util.deleteEmptyDirectories(outFile);
+ if(outFile.exists()){
+ if(!option.force){
+ throw new ARGException("Path already exists: "+outFile);
+ }
+ log("Deleting: "+outFile);
+ Util.deleteDir(outFile);
+ }
+ log("Refactoring ...\n"+option);
+ Refactor refactor=new Refactor(option);
+ refactor.run();
+ }
+ private static void logSameLine(String msg){
+ Logger.sameLine(getLogTag()+msg);
+ }
+ private static void log(String msg){
+ Logger.i(getLogTag()+msg);
+ }
+ private static String getLogTag(){
+ return "[REFACTOR] ";
+ }
+ public static boolean isCommand(String command){
+ if(Util.isEmpty(command)){
+ return false;
+ }
+ command=command.toLowerCase().trim();
+ return command.equals(ARG_SHORT) || command.equals(ARG_LONG);
+ }
+ public static final String ARG_SHORT="x";
+ public static final String ARG_LONG="refactor";
+ public static final String DESCRIPTION="Refactors obfuscated resource names";
}
diff --git a/src/main/java/com/reandroid/apkeditor/refactor/RefactorOptions.java b/src/main/java/com/reandroid/apkeditor/refactor/RefactorOptions.java
index c5055ebf..a71757b8 100644
--- a/src/main/java/com/reandroid/apkeditor/refactor/RefactorOptions.java
+++ b/src/main/java/com/reandroid/apkeditor/refactor/RefactorOptions.java
@@ -1,138 +1,138 @@
- /*
- * Copyright (C) 2022 github.com/REAndroid
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
+/*
+ * Copyright (C) 2022 github.com/REAndroid
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
package com.reandroid.apkeditor.refactor;
- import com.reandroid.apkeditor.APKEditor;
- import com.reandroid.apkeditor.Options;
- import com.reandroid.apkeditor.utils.StringHelper;
- import com.reandroid.commons.command.ARGException;
+import com.reandroid.apkeditor.APKEditor;
+import com.reandroid.apkeditor.Options;
+import com.reandroid.apkeditor.utils.StringHelper;
+import com.reandroid.commons.command.ARGException;
- import java.io.File;
+import java.io.File;
- public class RefactorOptions extends Options {
- public File publicXml;
- public boolean fixTypeNames;
- public boolean cleanMeta;
- public RefactorOptions(){
- super();
- }
- @Override
- public void parse(String[] args) throws ARGException {
- parseInput(args);
- parseOutput(args);
- parsePublicXml(args);
- parseFixTypes(args);
- parseKeepMeta(args);
+public class RefactorOptions extends Options {
+ public File publicXml;
+ public boolean fixTypeNames;
+ public boolean cleanMeta;
+ public RefactorOptions(){
+ super();
+ }
+ @Override
+ public void parse(String[] args) throws ARGException {
+ parseInput(args);
+ parseOutput(args);
+ parsePublicXml(args);
+ parseFixTypes(args);
+ parseKeepMeta(args);
- super.parse(args);
- }
- private void parseKeepMeta(String[] args) throws ARGException {
- cleanMeta = containsArg(ARG_cleanMeta, true, args);
- }
- private void parseFixTypes(String[] args) throws ARGException {
- fixTypeNames=containsArg(ARG_fix_types, true, args);
- }
- private void parsePublicXml(String[] args) throws ARGException {
- this.publicXml=null;
- File file=parseFile(ARG_public_xml, args);
- if(file==null){
- return;
- }
- if(!file.isFile()){
- throw new ARGException("No such file: "+file);
- }
- this.publicXml=file;
- }
- private void parseOutput(String[] args) throws ARGException {
- this.outputFile=null;
- File file=parseFile(ARG_output, args);
- if(file==null){
- file=getOutputApkFromInput(inputFile);
- }
- this.outputFile=file;
- }
- private File getOutputApkFromInput(File file){
- String name = file.getName();
- int i=name.lastIndexOf('.');
- if(i>0){
- name=name.substring(0, i);
- }
- name=name+"_refactored.apk";
- File dir=file.getParentFile();
- if(dir==null){
- return new File(name);
- }
- return new File(dir, name);
- }
- private void parseInput(String[] args) throws ARGException {
- this.inputFile=null;
- File file=parseFile(ARG_input, args);
- if(file==null){
- throw new ARGException("Missing input file");
- }
- if(!file.isFile()){
- throw new ARGException("No such file: "+file);
- }
- this.inputFile=file;
- }
- @Override
- public String toString(){
- StringBuilder builder=new StringBuilder();
- builder.append(" Input: ").append(inputFile);
- builder.append("\n Output: ").append(outputFile);
- if(publicXml!=null){
- builder.append("\n PublicXml: ").append(publicXml);
- }
- if(force){
- builder.append("\n Force: true");
- }
- if(cleanMeta){
- builder.append("\n Keep meta: true");
- }
- builder.append("\n ---------------------------- ");
- return builder.toString();
- }
- public static String getHelp(){
- StringBuilder builder=new StringBuilder();
- builder.append(Refactor.DESCRIPTION);
- builder.append("\nOptions:\n");
- String[][] table=new String[][]{
- new String[]{ARG_input, ARG_DESC_input},
- new String[]{ARG_output, ARG_DESC_output},
- new String[]{ARG_public_xml, ARG_DESC_public_xml}
- };
- StringHelper.printTwoColumns(builder, " ", 75, table);
- builder.append("\nFlags:\n");
- table=new String[][]{
- new String[]{ARG_fix_types, ARG_DESC_fix_types},
- new String[]{ARG_force, ARG_DESC_force},
- new String[]{ARG_cleanMeta, ARG_DESC_cleanMeta}
- };
- StringHelper.printTwoColumns(builder, " ", 75, table);
- String jar = APKEditor.getJarName();
- builder.append("\n\nExample-1:");
- builder.append("\n java -jar ").append(jar).append(" ").append(Refactor.ARG_SHORT).append(" ")
- .append(ARG_input).append(" path/to/input.apk");
- builder.append(" ").append(ARG_output).append(" path/to/out.apk");
- return builder.toString();
- }
+ super.parse(args);
+ }
+ private void parseKeepMeta(String[] args) throws ARGException {
+ cleanMeta = containsArg(ARG_cleanMeta, true, args);
+ }
+ private void parseFixTypes(String[] args) throws ARGException {
+ fixTypeNames=containsArg(ARG_fix_types, true, args);
+ }
+ private void parsePublicXml(String[] args) throws ARGException {
+ this.publicXml=null;
+ File file=parseFile(ARG_public_xml, args);
+ if(file==null){
+ return;
+ }
+ if(!file.isFile()){
+ throw new ARGException("No such file: "+file);
+ }
+ this.publicXml=file;
+ }
+ private void parseOutput(String[] args) throws ARGException {
+ this.outputFile=null;
+ File file=parseFile(ARG_output, args);
+ if(file==null){
+ file=getOutputApkFromInput(inputFile);
+ }
+ this.outputFile=file;
+ }
+ private File getOutputApkFromInput(File file){
+ String name = file.getName();
+ int i=name.lastIndexOf('.');
+ if(i>0){
+ name=name.substring(0, i);
+ }
+ name=name+"_refactored.apk";
+ File dir=file.getParentFile();
+ if(dir==null){
+ return new File(name);
+ }
+ return new File(dir, name);
+ }
+ private void parseInput(String[] args) throws ARGException {
+ this.inputFile=null;
+ File file=parseFile(ARG_input, args);
+ if(file==null){
+ throw new ARGException("Missing input file");
+ }
+ if(!file.isFile()){
+ throw new ARGException("No such file: "+file);
+ }
+ this.inputFile=file;
+ }
+ @Override
+ public String toString(){
+ StringBuilder builder=new StringBuilder();
+ builder.append(" Input: ").append(inputFile);
+ builder.append("\n Output: ").append(outputFile);
+ if(publicXml!=null){
+ builder.append("\n PublicXml: ").append(publicXml);
+ }
+ if(force){
+ builder.append("\n Force: true");
+ }
+ if(cleanMeta){
+ builder.append("\n Keep meta: true");
+ }
+ builder.append("\n ---------------------------- ");
+ return builder.toString();
+ }
+ public static String getHelp(){
+ StringBuilder builder=new StringBuilder();
+ builder.append(Refactor.DESCRIPTION);
+ builder.append("\nOptions:\n");
+ String[][] table=new String[][]{
+ new String[]{ARG_input, ARG_DESC_input},
+ new String[]{ARG_output, ARG_DESC_output},
+ new String[]{ARG_public_xml, ARG_DESC_public_xml}
+ };
+ StringHelper.printTwoColumns(builder, " ", Options.PRINT_WIDTH, table);
+ builder.append("\nFlags:\n");
+ table=new String[][]{
+ new String[]{ARG_fix_types, ARG_DESC_fix_types},
+ new String[]{ARG_force, ARG_DESC_force},
+ new String[]{ARG_cleanMeta, ARG_DESC_cleanMeta}
+ };
+ StringHelper.printTwoColumns(builder, " ", Options.PRINT_WIDTH, table);
+ String jar = APKEditor.getJarName();
+ builder.append("\n\nExample-1:");
+ builder.append("\n java -jar ").append(jar).append(" ").append(Refactor.ARG_SHORT).append(" ")
+ .append(ARG_input).append(" path/to/input.apk");
+ builder.append(" ").append(ARG_output).append(" path/to/out.apk");
+ return builder.toString();
+ }
- private static final String ARG_public_xml = "-public-xml";
- private static final String ARG_DESC_public_xml = "Path of resource ids xml file (public.xml)\nLoads names and applies to resources from 'public.xml' file";
+ private static final String ARG_public_xml = "-public-xml";
+ private static final String ARG_DESC_public_xml = "Path of resource ids xml file (public.xml)\nLoads names and applies to resources from 'public.xml' file";
- private static final String ARG_fix_types = "-fix-types";
- private static final String ARG_DESC_fix_types = "Corrects resource type names based on usages and values";
+ private static final String ARG_fix_types = "-fix-types";
+ private static final String ARG_DESC_fix_types = "Corrects resource type names based on usages and values";
}
diff --git a/src/main/java/com/reandroid/apkeditor/utils/StringHelper.java b/src/main/java/com/reandroid/apkeditor/utils/StringHelper.java
index 08081533..58985202 100644
--- a/src/main/java/com/reandroid/apkeditor/utils/StringHelper.java
+++ b/src/main/java/com/reandroid/apkeditor/utils/StringHelper.java
@@ -1,165 +1,170 @@
- /*
- * Copyright (C) 2022 github.com/REAndroid
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- package com.reandroid.apkeditor.utils;
+/*
+ * Copyright (C) 2022 github.com/REAndroid
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.reandroid.apkeditor.utils;
- import java.util.ArrayList;
- import java.util.Comparator;
- import java.util.List;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
- public class StringHelper {
- public static String trueOrNull(Boolean value){
- if(value == null){
- return null;
- }
- return trueOrNull(value.booleanValue());
- }
- public static String trueOrNull(boolean value){
- if(!value){
- return null;
- }
- return String.valueOf(true);
- }
- public static List sortAscending(List nameList){
- Comparator cmp=new Comparator() {
- @Override
- public int compare(String s1, String s2) {
- return s1.compareTo(s2);
- }
- };
- nameList.sort(cmp);
- return nameList;
- }
- public static void printNameAndValues(StringBuilder builder, String tab, int totalWidth, Object[][] objTable){
- printNameAndValues(builder, tab, "", totalWidth, objTable);
- }
- public static void printNameAndValues(StringBuilder builder, String tab, String separator, int totalWidth, Object[][] objTable){
- String[][] table = convertNameAndValue(objTable);
- if(table==null){
- return;
- }
- int leftWidth=0;
- for(String[] col:table){
- int len=col[0].length();
- if(len>leftWidth){
- leftWidth=len;
- }
- }
- int bnColumns=0;
- leftWidth=leftWidth+bnColumns;
- int maxRight=totalWidth-leftWidth;
- for(int i=0;i results = new ArrayList<>();
- for(Object[] objRow:table){
- String[] row = convertNameAndValueRow(objRow);
- if(row!=null){
- results.add(row);
- }
- }
- if(results.size()==0){
- return null;
- }
- return results.toArray(new String[0][]);
- }
- private static String[] convertNameAndValueRow(Object[] objRow){
- if(objRow==null){
- return null;
- }
- int len = objRow.length;
- if(len!=2){
- return null;
- }
- if(objRow[0] == null || objRow[1] == null){
- return null;
- }
- String[] result = new String[len];
- result[0] = objRow[0].toString();
- result[1] = objRow[1].toString();
- if(result[0] == null || result[1] == null){
- return null;
- }
- return result;
- }
- public static void printTwoColumns(StringBuilder builder, String tab, int totalWidth, String[][] table){
- int leftWidth=0;
- for(String[] col:table){
- int len=col[0].length();
- if(len>leftWidth){
- leftWidth=len;
- }
- }
- int bnColumns=3;
- leftWidth=leftWidth+bnColumns;
- int maxRight=totalWidth-leftWidth;
- for(int i=0;i 0 && rightWidth%maxRight==0)){
- builder.append('\n');
- builder.append(tab);
- fillSpace(builder, leftWidth+separator.length());
- rightWidth=0;
- }
- if(ch!='\n'){
- boolean skipFirstSpace=(rightWidth==0 && ch==' ');
- if(!skipFirstSpace){
- builder.append(ch);
- rightWidth++;
- }
- }
- }
- }
- private static void fillSpace(StringBuilder builder, int count){
- for(int i=0;i sortAscending(List nameList){
+ Comparator cmp=new Comparator() {
+ @Override
+ public int compare(String s1, String s2) {
+ return s1.compareTo(s2);
+ }
+ };
+ nameList.sort(cmp);
+ return nameList;
+ }
+ public static void printNameAndValues(StringBuilder builder, String tab, int totalWidth, Object[][] objTable){
+ printNameAndValues(builder, tab, "", totalWidth, objTable);
+ }
+ public static void printNameAndValues(StringBuilder builder, String tab, String separator, int totalWidth, Object[][] objTable){
+ String[][] table = convertNameAndValue(objTable);
+ if(table==null){
+ return;
+ }
+ int leftWidth=0;
+ for(String[] col:table){
+ int len=col[0].length();
+ if(len>leftWidth){
+ leftWidth=len;
+ }
+ }
+ int bnColumns=0;
+ leftWidth=leftWidth+bnColumns;
+ int maxRight=totalWidth-leftWidth;
+ for(int i=0;i results = new ArrayList<>();
+ for(Object[] objRow:table){
+ String[] row = convertNameAndValueRow(objRow);
+ if(row!=null){
+ results.add(row);
+ }
+ }
+ if(results.size()==0){
+ return null;
+ }
+ return results.toArray(new String[0][]);
+ }
+ private static String[] convertNameAndValueRow(Object[] objRow){
+ if(objRow==null){
+ return null;
+ }
+ int len = objRow.length;
+ if(len!=2){
+ return null;
+ }
+ if(objRow[0] == null || objRow[1] == null){
+ return null;
+ }
+ String[] result = new String[len];
+ result[0] = objRow[0].toString();
+ result[1] = objRow[1].toString();
+ if(result[0] == null || result[1] == null){
+ return null;
+ }
+ return result;
+ }
+ public static void printTwoColumns(StringBuilder builder, String tab, int totalWidth, String[][] table){
+ printTwoColumns(builder, tab, " ", totalWidth, table);
+ }
+ public static void printTwoColumns(StringBuilder builder, String tab, String columnSeparator, int totalWidth, String[][] table){
+ int leftWidth = 0;
+ for(String[] col:table){
+ int len = col[0].length();
+ if(len > leftWidth){
+ leftWidth = len;
+ }
+ }
+ int maxRight = totalWidth - leftWidth;
+ for(int i=0;i 0 && rightWidth%maxRight==0)){
+ builder.append('\n');
+ builder.append(tab);
+ fillSpace(builder, leftWidth+separator.length());
+ rightWidth = 0;
+ spacePrefixSeen = false;
+ }
+ if(ch!='\n'){
+ boolean skipFirstSpace=(rightWidth==0 && ch==' ');
+ if(!skipFirstSpace || spacePrefixSeen){
+ builder.append(ch);
+ rightWidth++;
+ }else{
+ spacePrefixSeen = true;
+ }
+ }
+ }
+ }
+ private static void fillSpace(StringBuilder builder, int count){
+ for(int i=0;i