|
|
@ -25,6 +25,9 @@ import jode.bytecode.InnerClassInfo; |
|
|
|
import jode.decompiler.*; |
|
|
|
import jode.decompiler.*; |
|
|
|
import java.util.zip.ZipOutputStream; |
|
|
|
import java.util.zip.ZipOutputStream; |
|
|
|
import java.util.zip.ZipEntry; |
|
|
|
import java.util.zip.ZipEntry; |
|
|
|
|
|
|
|
import gnu.getopt.LongOpt; |
|
|
|
|
|
|
|
import gnu.getopt.Getopt; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public class Decompiler { |
|
|
|
public class Decompiler { |
|
|
|
public final static int TAB_SIZE_MASK = 0x0f; |
|
|
|
public final static int TAB_SIZE_MASK = 0x0f; |
|
|
@ -47,10 +50,38 @@ public class Decompiler { |
|
|
|
OPTION_LVT | OPTION_INNER | OPTION_ANON | |
|
|
|
OPTION_LVT | OPTION_INNER | OPTION_ANON | |
|
|
|
OPTION_DECRYPT | OPTION_VERIFY | OPTION_CONTRAFO; |
|
|
|
OPTION_DECRYPT | OPTION_VERIFY | OPTION_CONTRAFO; |
|
|
|
|
|
|
|
|
|
|
|
public static final String[] optionNames = { |
|
|
|
private static final int OPTION_START=100; |
|
|
|
"lvt", "inner", "anonymous", "push", |
|
|
|
private static final int OPTION_END =200; |
|
|
|
"pretty", "decrypt", "onetime", "immediate", |
|
|
|
private static final LongOpt[] longOptions = new LongOpt[] { |
|
|
|
"verify", "contrafo" |
|
|
|
new LongOpt("cp", LongOpt.REQUIRED_ARGUMENT, null, 'c'), |
|
|
|
|
|
|
|
new LongOpt("classpath", LongOpt.REQUIRED_ARGUMENT, null, 'c'), |
|
|
|
|
|
|
|
new LongOpt("dest", LongOpt.REQUIRED_ARGUMENT, null, 'd'), |
|
|
|
|
|
|
|
new LongOpt("help", LongOpt.NO_ARGUMENT, null, 'h'), |
|
|
|
|
|
|
|
new LongOpt("version", LongOpt.NO_ARGUMENT, null, 'V'), |
|
|
|
|
|
|
|
new LongOpt("verbose", LongOpt.OPTIONAL_ARGUMENT, null, 'v'), |
|
|
|
|
|
|
|
new LongOpt("debug", LongOpt.OPTIONAL_ARGUMENT, null, 'D'), |
|
|
|
|
|
|
|
new LongOpt("import", LongOpt.REQUIRED_ARGUMENT, null, 'i'), |
|
|
|
|
|
|
|
new LongOpt("style", LongOpt.REQUIRED_ARGUMENT, null, 's'), |
|
|
|
|
|
|
|
new LongOpt("lvt", LongOpt.OPTIONAL_ARGUMENT, null, |
|
|
|
|
|
|
|
OPTION_START+0), |
|
|
|
|
|
|
|
new LongOpt("inner", LongOpt.OPTIONAL_ARGUMENT, null, |
|
|
|
|
|
|
|
OPTION_START+1), |
|
|
|
|
|
|
|
new LongOpt("anonymous", LongOpt.OPTIONAL_ARGUMENT, null, |
|
|
|
|
|
|
|
OPTION_START+2), |
|
|
|
|
|
|
|
new LongOpt("push", LongOpt.OPTIONAL_ARGUMENT, null, |
|
|
|
|
|
|
|
OPTION_START+3), |
|
|
|
|
|
|
|
new LongOpt("pretty", LongOpt.OPTIONAL_ARGUMENT, null, |
|
|
|
|
|
|
|
OPTION_START+4), |
|
|
|
|
|
|
|
new LongOpt("decrypt", LongOpt.OPTIONAL_ARGUMENT, null, |
|
|
|
|
|
|
|
OPTION_START+5), |
|
|
|
|
|
|
|
new LongOpt("onetime", LongOpt.OPTIONAL_ARGUMENT, null, |
|
|
|
|
|
|
|
OPTION_START+6), |
|
|
|
|
|
|
|
new LongOpt("immediate", LongOpt.OPTIONAL_ARGUMENT, null, |
|
|
|
|
|
|
|
OPTION_START+7), |
|
|
|
|
|
|
|
new LongOpt("verify", LongOpt.OPTIONAL_ARGUMENT, null, |
|
|
|
|
|
|
|
OPTION_START+8), |
|
|
|
|
|
|
|
new LongOpt("contrafo", LongOpt.OPTIONAL_ARGUMENT, null, |
|
|
|
|
|
|
|
OPTION_START+9) |
|
|
|
}; |
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
public static int outputStyle = SUN_STYLE; |
|
|
|
public static int outputStyle = SUN_STYLE; |
|
|
@ -58,52 +89,69 @@ public class Decompiler { |
|
|
|
public static void usage() { |
|
|
|
public static void usage() { |
|
|
|
PrintWriter err = GlobalOptions.err; |
|
|
|
PrintWriter err = GlobalOptions.err; |
|
|
|
err.println("Version: " + GlobalOptions.version); |
|
|
|
err.println("Version: " + GlobalOptions.version); |
|
|
|
err.print("use: jode [-v]" |
|
|
|
err.println("Usage: java jode.Decompiler [OPTIONS]... [CLASSES]..."); |
|
|
|
+"[--cp <classpath>][--dest <destdir>]" |
|
|
|
err.println(" -h, --help "+ |
|
|
|
+"[--import <pkglimit> <clslimit>]"); |
|
|
|
"show this information."); |
|
|
|
for (int i=0; i < optionNames.length; i++) |
|
|
|
err.println(" -v, --verbose "+ |
|
|
|
err.print("[--[no]"+optionNames[i]+"]"); |
|
|
|
|
|
|
|
err.println("[--debug=...] class1 [class2 ...]"); |
|
|
|
|
|
|
|
err.println("\t-v "+ |
|
|
|
|
|
|
|
"be verbose (multiple times means more verbose)."); |
|
|
|
"be verbose (multiple times means more verbose)."); |
|
|
|
err.println("\t--cp <classpath> "+ |
|
|
|
err.println(" -c, --classpath <path> "+ |
|
|
|
"search for classes in specified classpath."); |
|
|
|
"search for classes in specified classpath."); |
|
|
|
err.println("\t "+ |
|
|
|
err.println(" "+ |
|
|
|
"The paths should be separated by ','."); |
|
|
|
"The directories should be separated by ','."); |
|
|
|
err.println("\t--dest <destdir> "+ |
|
|
|
err.println(" -d, --dest <dir> "+ |
|
|
|
"write decompiled files to disk into directory destdir."); |
|
|
|
"write decompiled files to disk into directory destdir."); |
|
|
|
err.println("\t--style {sun|gnu}"+ |
|
|
|
err.println(" -s, --style {sun|gnu} "+ |
|
|
|
" specifies indentation style"); |
|
|
|
"specify indentation style"); |
|
|
|
err.println("\t--import <pkglimit> <clslimit>"); |
|
|
|
err.println(" -i, --import <pkglimit>,<clslimit>"); |
|
|
|
err.println("\t "+ |
|
|
|
err.println(" "+ |
|
|
|
"import classes used more than clslimit times"); |
|
|
|
"import classes used more than clslimit times"); |
|
|
|
err.println("\t "+ |
|
|
|
err.println(" "+ |
|
|
|
"and packages with more then pkglimit used classes"); |
|
|
|
"and packages with more then pkglimit used classes."); |
|
|
|
err.println("\t--[no]inner "+ |
|
|
|
err.println(" "+ |
|
|
|
"[don't] decompile inner classes."); |
|
|
|
"Limit 0 means, never import, default is 0,1."); |
|
|
|
err.println("\t--[no]anonymous "+ |
|
|
|
|
|
|
|
"[don't] decompile anonymous classes."); |
|
|
|
err.println("The following options can be turned on or off with `yes' or `no' argument."); |
|
|
|
err.println("\t--[no]contrafo "+ |
|
|
|
err.println(" --inner "+ |
|
|
|
"[don't] transform constructors of inner classes."); |
|
|
|
"decompile inner classes (default)."); |
|
|
|
err.println("\t--[no]lvt "+ |
|
|
|
err.println(" --anonymous "+ |
|
|
|
"[don't] use the local variable table."); |
|
|
|
"decompile anonymous classes (default)."); |
|
|
|
err.println("\t--[no]pretty "+ |
|
|
|
err.println(" --contrafo "+ |
|
|
|
"[don't] use `pretty' names for local variables."); |
|
|
|
"transform constructors of inner classes (default)."); |
|
|
|
err.println("\t--[no]push "+ |
|
|
|
err.println(" --lvt "+ |
|
|
|
"[replace] PUSH instructions [with compilable code]."); |
|
|
|
"use the local variable table (default)."); |
|
|
|
err.println("\t--[no]decrypt "+ |
|
|
|
err.println(" --pretty "+ |
|
|
|
"[don't] try to decrypt encrypted strings."); |
|
|
|
"use `pretty' names for local variables."); |
|
|
|
err.println("\t--[no]onetime "+ |
|
|
|
err.println(" --push "+ |
|
|
|
"[don't] remove locals, that are used only one time."); |
|
|
|
"allow PUSH instructions in output."); |
|
|
|
err.println("\t--[no]immediate "+ |
|
|
|
err.println(" --decrypt "+ |
|
|
|
"[don't] output source immediately with wrong import."); |
|
|
|
"decrypt encrypted strings (default)."); |
|
|
|
err.println("\t--[no]verify "+ |
|
|
|
err.println(" --onetime "+ |
|
|
|
"[don't] verify code before decompiling it."); |
|
|
|
"remove locals, that are used only one time."); |
|
|
|
|
|
|
|
err.println(" --immediate "+ |
|
|
|
|
|
|
|
"output source immediately (may produce buggy code)."); |
|
|
|
|
|
|
|
err.println(" --verify "+ |
|
|
|
|
|
|
|
"verify code before decompiling it."); |
|
|
|
err.println("Debugging options, mainly used to debug this decompiler:"); |
|
|
|
err.println("Debugging options, mainly used to debug this decompiler:"); |
|
|
|
err.println("\t--debug=... "+ |
|
|
|
err.println(" -D, --debug=... "+ |
|
|
|
"use --debug=help for more information."); |
|
|
|
"use --debug=help for more information."); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public static boolean handleOption(int option, int longind, String arg) { |
|
|
|
|
|
|
|
if (arg == null) |
|
|
|
|
|
|
|
options ^= 1 << option; |
|
|
|
|
|
|
|
else if ("yes".startsWith(arg) || arg.equals("on")) |
|
|
|
|
|
|
|
options |= 1 << option; |
|
|
|
|
|
|
|
else if ("no".startsWith(arg) || arg.equals("off")) |
|
|
|
|
|
|
|
options &= ~(1 << option); |
|
|
|
|
|
|
|
else { |
|
|
|
|
|
|
|
GlobalOptions.err.println |
|
|
|
|
|
|
|
("jode.Decompiler: option --"+longOptions[longind].getName() |
|
|
|
|
|
|
|
+" takes one of `yes', `no', `on', `off' as parameter"); |
|
|
|
|
|
|
|
return false; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
return true; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
public static boolean skipClass(ClassInfo clazz) { |
|
|
|
public static boolean skipClass(ClassInfo clazz) { |
|
|
|
InnerClassInfo[] outers = clazz.getOuterClasses(); |
|
|
|
InnerClassInfo[] outers = clazz.getOuterClasses(); |
|
|
|
if (outers != null) { |
|
|
|
if (outers != null) { |
|
|
@ -117,99 +165,123 @@ public class Decompiler { |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
public static void main(String[] params) { |
|
|
|
public static void main(String[] params) { |
|
|
|
int i; |
|
|
|
if (params.length == 0) { |
|
|
|
|
|
|
|
usage(); |
|
|
|
|
|
|
|
return; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
String classPath = System.getProperty("java.class.path") |
|
|
|
String classPath = System.getProperty("java.class.path") |
|
|
|
.replace(File.pathSeparatorChar, SearchPath.pathSeparatorChar); |
|
|
|
.replace(File.pathSeparatorChar, SearchPath.pathSeparatorChar); |
|
|
|
File destDir = null; |
|
|
|
String destDir = null; |
|
|
|
ZipOutputStream destZip = null; |
|
|
|
|
|
|
|
int importPackageLimit = ImportHandler.DEFAULT_PACKAGE_LIMIT; |
|
|
|
int importPackageLimit = ImportHandler.DEFAULT_PACKAGE_LIMIT; |
|
|
|
int importClassLimit = ImportHandler.DEFAULT_CLASS_LIMIT;; |
|
|
|
int importClassLimit = ImportHandler.DEFAULT_CLASS_LIMIT;; |
|
|
|
|
|
|
|
|
|
|
|
GlobalOptions.err.println(GlobalOptions.copyright); |
|
|
|
GlobalOptions.err.println(GlobalOptions.copyright); |
|
|
|
for (i=0; i<params.length && params[i].startsWith("-"); i++) { |
|
|
|
|
|
|
|
if (params[i].equals("-v")) |
|
|
|
boolean errorInParams = false; |
|
|
|
GlobalOptions.verboseLevel++; |
|
|
|
Getopt g = new Getopt("jode.Decompiler", params, "hVvc:d:D:i:s:", |
|
|
|
else if (params[i].equals("--dest")) |
|
|
|
longOptions, true); |
|
|
|
destDir = new File(params[++i]); |
|
|
|
for (int opt = g.getopt(); opt != -1; opt = g.getopt()) { |
|
|
|
else if (params[i].startsWith("--debug")) { |
|
|
|
switch(opt) { |
|
|
|
String flags; |
|
|
|
case 0: |
|
|
|
if (params[i].startsWith("--debug=")) { |
|
|
|
break; |
|
|
|
flags = params[i].substring(8); |
|
|
|
case 'h': |
|
|
|
} else if (params[i].length() != 7) { |
|
|
|
|
|
|
|
usage(); |
|
|
|
usage(); |
|
|
|
return; |
|
|
|
errorInParams = true; |
|
|
|
} else { |
|
|
|
break; |
|
|
|
flags = params[++i]; |
|
|
|
case 'V': |
|
|
|
} |
|
|
|
GlobalOptions.err.println(GlobalOptions.version); |
|
|
|
GlobalOptions.setDebugging(flags); |
|
|
|
break; |
|
|
|
} else if (params[i].equals("--style")) { |
|
|
|
case 'c': |
|
|
|
String style = params[++i]; |
|
|
|
classPath = g.getOptarg(); |
|
|
|
if (style.equals("sun")) |
|
|
|
break; |
|
|
|
outputStyle = SUN_STYLE; |
|
|
|
case 'd': |
|
|
|
else if (style.equals("gnu")) |
|
|
|
destDir = g.getOptarg(); |
|
|
|
outputStyle = GNU_STYLE; |
|
|
|
break; |
|
|
|
|
|
|
|
case 'v': { |
|
|
|
|
|
|
|
String arg = g.getOptarg(); |
|
|
|
|
|
|
|
if (arg == null) |
|
|
|
|
|
|
|
GlobalOptions.verboseLevel++; |
|
|
|
else { |
|
|
|
else { |
|
|
|
GlobalOptions.err.println("Unknown style: "+style); |
|
|
|
try { |
|
|
|
usage(); |
|
|
|
GlobalOptions.verboseLevel = Integer.parseInt(arg); |
|
|
|
return; |
|
|
|
} catch (NumberFormatException ex) { |
|
|
|
|
|
|
|
GlobalOptions.err.println |
|
|
|
|
|
|
|
("jode.Decompiler: Argument `" |
|
|
|
|
|
|
|
+arg+"' to --verbose must be numeric:"); |
|
|
|
|
|
|
|
errorInParams = true; |
|
|
|
} |
|
|
|
} |
|
|
|
} else if (params[i].equals("--import")) { |
|
|
|
|
|
|
|
importPackageLimit = Integer.parseInt(params[++i]); |
|
|
|
|
|
|
|
importClassLimit = Integer.parseInt(params[++i]); |
|
|
|
|
|
|
|
} else if (params[i].equals("--cp")) { |
|
|
|
|
|
|
|
classPath = params[++i]; |
|
|
|
|
|
|
|
} else if (params[i].equals("--")) { |
|
|
|
|
|
|
|
i++; |
|
|
|
|
|
|
|
break; |
|
|
|
|
|
|
|
} else { |
|
|
|
|
|
|
|
if (params[i].startsWith("--")) { |
|
|
|
|
|
|
|
boolean negated = false; |
|
|
|
|
|
|
|
String optionName = params[i].substring(2); |
|
|
|
|
|
|
|
if (optionName.startsWith("no")) { |
|
|
|
|
|
|
|
optionName = optionName.substring(2); |
|
|
|
|
|
|
|
negated = true; |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
int index = -1; |
|
|
|
|
|
|
|
for (int j=0; j < optionNames.length; j++) { |
|
|
|
|
|
|
|
if (optionNames[j].startsWith(optionName)) { |
|
|
|
|
|
|
|
if (optionNames[j].equals(optionName)) { |
|
|
|
|
|
|
|
index = j; |
|
|
|
|
|
|
|
break; |
|
|
|
break; |
|
|
|
} |
|
|
|
} |
|
|
|
if (index == -1) { |
|
|
|
case 'D': { |
|
|
|
index = j; |
|
|
|
String arg = g.getOptarg(); |
|
|
|
} else { |
|
|
|
if (arg == null) |
|
|
|
index = -2; |
|
|
|
arg = "help"; |
|
|
|
|
|
|
|
errorInParams |= !GlobalOptions.setDebugging(arg); |
|
|
|
break; |
|
|
|
break; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
case 's': { |
|
|
|
|
|
|
|
String arg = g.getOptarg(); |
|
|
|
|
|
|
|
if ("sun".startsWith(arg)) |
|
|
|
|
|
|
|
outputStyle = SUN_STYLE; |
|
|
|
|
|
|
|
else if ("gnu".startsWith(arg)) |
|
|
|
|
|
|
|
outputStyle = GNU_STYLE; |
|
|
|
|
|
|
|
else { |
|
|
|
|
|
|
|
GlobalOptions.err.println |
|
|
|
|
|
|
|
("jode.Decompiler: Unknown style `"+arg+"'."); |
|
|
|
|
|
|
|
errorInParams = true; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
break; |
|
|
|
} |
|
|
|
} |
|
|
|
if (index >= 0) { |
|
|
|
case 'i': { |
|
|
|
if (negated) |
|
|
|
String arg = g.getOptarg(); |
|
|
|
options &= ~(1<< index); |
|
|
|
int comma = arg.indexOf(','); |
|
|
|
else |
|
|
|
try { |
|
|
|
options |= 1 << index; |
|
|
|
int packLimit = Integer.parseInt(arg.substring(0, comma)); |
|
|
|
continue; |
|
|
|
if (packLimit == 0) |
|
|
|
|
|
|
|
packLimit = Integer.MAX_VALUE; |
|
|
|
|
|
|
|
if (packLimit < 0) |
|
|
|
|
|
|
|
throw new IllegalArgumentException(); |
|
|
|
|
|
|
|
int clazzLimit = Integer.parseInt(arg.substring(comma+1)); |
|
|
|
|
|
|
|
if (clazzLimit == 0) |
|
|
|
|
|
|
|
clazzLimit = Integer.MAX_VALUE; |
|
|
|
|
|
|
|
if (clazzLimit < 0) |
|
|
|
|
|
|
|
throw new IllegalArgumentException(); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
importPackageLimit = packLimit; |
|
|
|
|
|
|
|
importClassLimit = clazzLimit; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
} catch (RuntimeException ex) { |
|
|
|
|
|
|
|
GlobalOptions.err.println |
|
|
|
|
|
|
|
("jode.Decompiler: Invalid argument for -i option."); |
|
|
|
|
|
|
|
errorInParams = true; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
break; |
|
|
|
} |
|
|
|
} |
|
|
|
if (!params[i].startsWith("-h") && !params[i].equals("--help")) |
|
|
|
default: |
|
|
|
GlobalOptions.err.println("Unknown option: "+params[i]); |
|
|
|
if (opt >= OPTION_START && opt <= OPTION_END) { |
|
|
|
usage(); |
|
|
|
errorInParams |= !handleOption(opt-OPTION_START, |
|
|
|
return; |
|
|
|
g.getLongind(), |
|
|
|
|
|
|
|
g.getOptarg()); |
|
|
|
|
|
|
|
} else |
|
|
|
|
|
|
|
errorInParams = true; |
|
|
|
|
|
|
|
break; |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
if (i == params.length) { |
|
|
|
if (errorInParams) |
|
|
|
usage(); |
|
|
|
|
|
|
|
return; |
|
|
|
return; |
|
|
|
} |
|
|
|
ClassInfo.setClassPath(classPath.toString()); |
|
|
|
|
|
|
|
|
|
|
|
ClassInfo.setClassPath(classPath); |
|
|
|
|
|
|
|
ImportHandler imports = new ImportHandler(importPackageLimit, |
|
|
|
ImportHandler imports = new ImportHandler(importPackageLimit, |
|
|
|
importClassLimit); |
|
|
|
importClassLimit); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ZipOutputStream destZip = null; |
|
|
|
TabbedPrintWriter writer = null; |
|
|
|
TabbedPrintWriter writer = null; |
|
|
|
if (destDir == null) |
|
|
|
if (destDir == null) |
|
|
|
writer = new TabbedPrintWriter(System.out, imports); |
|
|
|
writer = new TabbedPrintWriter(System.out, imports); |
|
|
|
else if (destDir.getName().endsWith(".zip")) { |
|
|
|
else if (destDir.toLowerCase().endsWith(".zip") |
|
|
|
|
|
|
|
|| destDir.toLowerCase().endsWith(".jar")) { |
|
|
|
try { |
|
|
|
try { |
|
|
|
destZip = new ZipOutputStream(new FileOutputStream(destDir)); |
|
|
|
destZip = new ZipOutputStream(new FileOutputStream(destDir)); |
|
|
|
} catch (IOException ex) { |
|
|
|
} catch (IOException ex) { |
|
|
@ -220,7 +292,7 @@ public class Decompiler { |
|
|
|
writer = new TabbedPrintWriter(new BufferedOutputStream(destZip), |
|
|
|
writer = new TabbedPrintWriter(new BufferedOutputStream(destZip), |
|
|
|
imports, false); |
|
|
|
imports, false); |
|
|
|
} |
|
|
|
} |
|
|
|
for (; i< params.length; i++) { |
|
|
|
for (int i= g.getOptind(); i< params.length; i++) { |
|
|
|
try { |
|
|
|
try { |
|
|
|
ClassInfo clazz; |
|
|
|
ClassInfo clazz; |
|
|
|
try { |
|
|
|
try { |
|
|
@ -238,7 +310,7 @@ public class Decompiler { |
|
|
|
if (destZip != null) { |
|
|
|
if (destZip != null) { |
|
|
|
writer.flush(); |
|
|
|
writer.flush(); |
|
|
|
destZip.putNextEntry(new ZipEntry(filename)); |
|
|
|
destZip.putNextEntry(new ZipEntry(filename)); |
|
|
|
} else if (destDir != null) { |
|
|
|
} else if (writer == null) { |
|
|
|
File file = new File (destDir, filename); |
|
|
|
File file = new File (destDir, filename); |
|
|
|
File directory = new File(file.getParent()); |
|
|
|
File directory = new File(file.getParent()); |
|
|
|
if (!directory.exists() && !directory.mkdirs()) { |
|
|
|
if (!directory.exists() && !directory.mkdirs()) { |
|
|
|