Repository: ibotpeaches/apktool
Issue: ApkDecoder locks input file

Running

```
        ApkDecoder decoder = new ApkDecoder();
        try {
            decoder.setDecodeSources(ApkDecoder.DECODE_SOURCES_NONE);
            decoder.setDecodeResources(ApkDecoder.DECODE_RESOURCES_FULL);
            decoder.setApkFile(new File("i.apk"));
            decoder.setOutDir(new File("out"));
            decoder.setAnalysisMode(true, true);
            decoder.decode();
            Thread.sleep(60000 * 100);
        } catch (Exception e) {
            e.printStackTrace();
        }
```

causes Apktool to lock the input file under Windows until the JVM is exited.


Changed files:
brut.apktool/apktool-cli/src/main/java/brut/apktool/Main.java, brut.apktool/apktool-lib/src/main/java/brut/androlib/Androlib.java, brut.j.dir/src/main/java/brut/directory/ExtFile.java, brut.j.dir/src/main/java/brut/directory/Directory.java, brut.apktool/apktool-lib/src/main/java/brut/androlib/ApkDecoder.java, brut.j.dir/src/main/java/brut/directory/ZipRODirectory.java, brut.apktool/apktool-lib/src/main/java/brut/androlib/res/AndrolibResources.java, brut.j.dir/src/main/java/brut/directory/AbstractDirectory.java

diff --git a/brut.apktool/apktool-cli/src/main/java/brut/apktool/Main.java b/brut.apktool/apktool-cli/src/main/java/brut/apktool/Main.java
index 74470801..6e292713 100644
--- a/brut.apktool/apktool-cli/src/main/java/brut/apktool/Main.java
+++ b/brut.apktool/apktool-cli/src/main/java/brut/apktool/Main.java
@@ -187,6 +187,11 @@ public class Main {
         } catch (DirectoryException ex) {
             System.err.println("Could not modify internal dex files. Please ensure you have permission.");
             System.exit(1);
+        } finally {
+            try {
+                decoder.close();
+            } catch (IOException e) {
+            }
         }
 
     }
diff --git a/brut.apktool/apktool-lib/src/main/java/brut/androlib/Androlib.java b/brut.apktool/apktool-lib/src/main/java/brut/androlib/Androlib.java
index 8a1aaf55..d51cb8d4 100644
--- a/brut.apktool/apktool-lib/src/main/java/brut/androlib/Androlib.java
+++ b/brut.apktool/apktool-lib/src/main/java/brut/androlib/Androlib.java
@@ -741,6 +741,10 @@ public class Androlib {
         return files;
     }
 
+    public void close() throws IOException {
+        mAndRes.close();
+    }
+
     private final static Logger LOGGER = Logger.getLogger(Androlib.class.getName());
 
     private final static String SMALI_DIRNAME = "smali";
diff --git a/brut.apktool/apktool-lib/src/main/java/brut/androlib/ApkDecoder.java b/brut.apktool/apktool-lib/src/main/java/brut/androlib/ApkDecoder.java
index aa2ca555..72a10488 100644
--- a/brut.apktool/apktool-lib/src/main/java/brut/androlib/ApkDecoder.java
+++ b/brut.apktool/apktool-lib/src/main/java/brut/androlib/ApkDecoder.java
@@ -60,6 +60,14 @@ public class ApkDecoder {
     }
 
     public void setApkFile(File apkFile) {
+        if (mApkFile != null)
+        {
+            try {
+                mApkFile.close();
+            } catch (IOException e) {
+            }
+        }
+
         mApkFile = new ExtFile(apkFile);
         mResTable = null;
     }
@@ -73,93 +81,100 @@ public class ApkDecoder {
     }
 
     public void decode() throws AndrolibException, IOException, DirectoryException {
-        File outDir = getOutDir();
-        AndrolibResources.sKeepBroken = mKeepBrokenResources;
-
-        if (!mForceDelete && outDir.exists()) {
-            throw new OutDirExistsException();
-        }
-
-        if (!mApkFile.isFile() || !mApkFile.canRead()) {
-            throw new InFileNotFoundException();
-        }
-
         try {
-            OS.rmdir(outDir);
-        } catch (BrutException ex) {
-            throw new AndrolibException(ex);
-        }
-        outDir.mkdirs();
+            File outDir = getOutDir();
+            AndrolibResources.sKeepBroken = mKeepBrokenResources;
 
-        LOGGER.info("Using Apktool " + Androlib.getVersion() + " on " + mApkFile.getName());
+            if (!mForceDelete && outDir.exists()) {
+                throw new OutDirExistsException();
+            }
 
-        if (hasResources()) {
-            switch (mDecodeResources) {
-                case DECODE_RESOURCES_NONE:
-                    mAndrolib.decodeResourcesRaw(mApkFile, outDir);
-                    break;
-                case DECODE_RESOURCES_FULL:
-                    setTargetSdkVersion();
-                    setAnalysisMode(mAnalysisMode, true);
+            if (!mApkFile.isFile() || !mApkFile.canRead()) {
+                throw new InFileNotFoundException();
+            }
 
-                    if (hasManifest()) {
-                        mAndrolib.decodeManifestWithResources(mApkFile, outDir, getResTable());
-                    }
-                    mAndrolib.decodeResourcesFull(mApkFile, outDir, getResTable());
-                    break;
+            try {
+                OS.rmdir(outDir);
+            } catch (BrutException ex) {
+                throw new AndrolibException(ex);
             }
-        } else {
-            // if there's no resources.asrc, decode the manifest without looking
-            // up attribute references
-            if (hasManifest()) {
+            outDir.mkdirs();
+
+            LOGGER.info("Using Apktool " + Androlib.getVersion() + " on " + mApkFile.getName());
+
+            if (hasResources()) {
                 switch (mDecodeResources) {
                     case DECODE_RESOURCES_NONE:
-                        mAndrolib.decodeManifestRaw(mApkFile, outDir);
+                        mAndrolib.decodeResourcesRaw(mApkFile, outDir);
                         break;
                     case DECODE_RESOURCES_FULL:
-                        mAndrolib.decodeManifestFull(mApkFile, outDir,
-                                getResTable());
+                        setTargetSdkVersion();
+                        setAnalysisMode(mAnalysisMode, true);
+
+                        if (hasManifest()) {
+                            mAndrolib.decodeManifestWithResources(mApkFile, outDir, getResTable());
+                        }
+                        mAndrolib.decodeResourcesFull(mApkFile, outDir, getResTable());
                         break;
                 }
+            } else {
+                // if there's no resources.asrc, decode the manifest without looking
+                // up attribute references
+                if (hasManifest()) {
+                    switch (mDecodeResources) {
+                        case DECODE_RESOURCES_NONE:
+                            mAndrolib.decodeManifestRaw(mApkFile, outDir);
+                            break;
+                        case DECODE_RESOURCES_FULL:
+                            mAndrolib.decodeManifestFull(mApkFile, outDir,
+                                    getResTable());
+                            break;
+                    }
+                }
             }
-        }
 
-        if (hasSources()) {
-            switch (mDecodeSources) {
-                case DECODE_SOURCES_NONE:
-                    mAndrolib.decodeSourcesRaw(mApkFile, outDir, "classes.dex");
-                    break;
-                case DECODE_SOURCES_SMALI:
-                    mAndrolib.decodeSourcesSmali(mApkFile, outDir, "classes.dex", mBakDeb, mApi);
-                    break;
+            if (hasSources()) {
+                switch (mDecodeSources) {
+                    case DECODE_SOURCES_NONE:
+                        mAndrolib.decodeSourcesRaw(mApkFile, outDir, "classes.dex");
+                        break;
+                    case DECODE_SOURCES_SMALI:
+                        mAndrolib.decodeSourcesSmali(mApkFile, outDir, "classes.dex", mBakDeb, mApi);
+                        break;
+                }
             }
-        }
 
-        if (hasMultipleSources()) {
-            // foreach unknown dex file in root, lets disassemble it
-            Set<String> files = mApkFile.getDirectory().getFiles(true);
-            for (String file : files) {
-                if (file.endsWith(".dex")) {
-                    if (! file.equalsIgnoreCase("classes.dex")) {
-                        switch(mDecodeSources) {
-                            case DECODE_SOURCES_NONE:
-                                mAndrolib.decodeSourcesRaw(mApkFile, outDir, file);
-                                break;
-                            case DECODE_SOURCES_SMALI:
-                                mAndrolib.decodeSourcesSmali(mApkFile, outDir, file, mBakDeb, mApi);
-                                break;
+            if (hasMultipleSources()) {
+                // foreach unknown dex file in root, lets disassemble it
+                Set<String> files = mApkFile.getDirectory().getFiles(true);
+                for (String file : files) {
+                    if (file.endsWith(".dex")) {
+                        if (! file.equalsIgnoreCase("classes.dex")) {
+                            switch(mDecodeSources) {
+                                case DECODE_SOURCES_NONE:
+                                    mAndrolib.decodeSourcesRaw(mApkFile, outDir, file);
+                                    break;
+                                case DECODE_SOURCES_SMALI:
+                                    mAndrolib.decodeSourcesSmali(mApkFile, outDir, file, mBakDeb, mApi);
+                                    break;
+                            }
                         }
                     }
                 }
             }
-        }
 
-        mAndrolib.decodeRawFiles(mApkFile, outDir);
-        mAndrolib.decodeUnknownFiles(mApkFile, outDir, mResTable);
-        mUncompressedFiles = new ArrayList<String>();
-        mAndrolib.recordUncompressedFiles(mApkFile, mUncompressedFiles);
-        mAndrolib.writeOriginalFiles(mApkFile, outDir);
-        writeMetaFile();
+            mAndrolib.decodeRawFiles(mApkFile, outDir);
+            mAndrolib.decodeUnknownFiles(mApkFile, outDir, mResTable);
+            mUncompressedFiles = new ArrayList<String>();
+            mAndrolib.recordUncompressedFiles(mApkFile, mUncompressedFiles);
+            mAndrolib.writeOriginalFiles(mApkFile, outDir);
+            writeMetaFile();
+        } finally {
+            try {
+                mApkFile.close();
+            } catch (IOException e) {
+            }
+        }
     }
 
     public void setDecodeSources(short mode) throws AndrolibException {
@@ -273,6 +288,10 @@ public class ApkDecoder {
         }
     }
 
+    public void close() throws IOException {
+        mAndrolib.close();
+    }
+
     public final static short DECODE_SOURCES_NONE = 0x0000;
     public final static short DECODE_SOURCES_SMALI = 0x0001;
 
diff --git a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/AndrolibResources.java b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/AndrolibResources.java
index ae884321..3b308422 100644
--- a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/AndrolibResources.java
+++ b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/AndrolibResources.java
@@ -121,7 +121,8 @@ final public class AndrolibResources {
         File apk = getFrameworkApk(id, frameTag);
 
         LOGGER.info("Loading resource table from file: " + apk);
-        ResPackage[] pkgs = getResPackagesFromApk(new ExtFile(apk), resTable, true);
+        mFramework = new ExtFile(apk);
+        ResPackage[] pkgs = getResPackagesFromApk(mFramework, resTable, true);
 
         ResPackage pkg;
         if (pkgs.length > 1) {
@@ -555,15 +556,23 @@ final public class AndrolibResources {
     private ResPackage[] getResPackagesFromApk(ExtFile apkFile,ResTable resTable, boolean keepBroken)
             throws AndrolibException {
         try {
-            BufferedInputStream bfi = new BufferedInputStream(apkFile.getDirectory().getFileInput("resources.arsc"));
-            return ARSCDecoder.decode(bfi, false, keepBroken, resTable).getPackages();
+            Directory dir = apkFile.getDirectory();
+            BufferedInputStream bfi = new BufferedInputStream(dir.getFileInput("resources.arsc"));
+            try {
+                return ARSCDecoder.decode(bfi, false, keepBroken, resTable).getPackages();
+            } finally {
+                try {
+                    bfi.close();
+                } catch (IOException e) {
+                }
+            }
         } catch (DirectoryException ex) {
             throw new AndrolibException("Could not load resources.arsc from file: " + apkFile, ex);
         }
     }
 
     public File getFrameworkApk(int id, String frameTag)
-            throws AndrolibException {
+            throws AndrolibException {  
         File dir = getFrameworkDir();
         File apk;
 
@@ -810,6 +819,10 @@ final public class AndrolibResources {
         }
     }
 
+    public void close() throws IOException {
+        mFramework.close();
+    }
+
     public ApkOptions apkOptions;
 
     // TODO: dirty static hack. I have to refactor decoding mechanisms.
@@ -819,6 +832,8 @@ final public class AndrolibResources {
 
     private File mFrameworkDirectory = null;
 
+    private ExtFile mFramework = null;
+
     private String mMinSdkVersion = null;
     private String mMaxSdkVersion = null;
     private String mTargetSdkVersion = null;
diff --git a/brut.j.dir/src/main/java/brut/directory/AbstractDirectory.java b/brut.j.dir/src/main/java/brut/directory/AbstractDirectory.java
index c2befcbe..ea906c7a 100644
--- a/brut.j.dir/src/main/java/brut/directory/AbstractDirectory.java
+++ b/brut.j.dir/src/main/java/brut/directory/AbstractDirectory.java
@@ -17,6 +17,7 @@
 package brut.directory;
 
 import java.io.File;
+import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
 import java.util.LinkedHashMap;
@@ -235,6 +236,11 @@ public abstract class AbstractDirectory implements Directory {
         return dirs;
     }
 
+
+    public void close() throws IOException {
+
+    }
+
     private SubPath getSubPath(String path) throws PathNotExist {
         ParsedPath parsed = parsePath(path);
         if (parsed.dir == null) {
diff --git a/brut.j.dir/src/main/java/brut/directory/Directory.java b/brut.j.dir/src/main/java/brut/directory/Directory.java
index 0199135d..e7a972be 100644
--- a/brut.j.dir/src/main/java/brut/directory/Directory.java
+++ b/brut.j.dir/src/main/java/brut/directory/Directory.java
@@ -50,5 +50,8 @@ public interface Directory {
     public int getCompressionLevel(String fileName)
             throws DirectoryException;
 
+
+    public void close() throws IOException;
+    
     public final char separator = '/';
 }
diff --git a/brut.j.dir/src/main/java/brut/directory/ExtFile.java b/brut.j.dir/src/main/java/brut/directory/ExtFile.java
index 583c3fa1..008e1b45 100644
--- a/brut.j.dir/src/main/java/brut/directory/ExtFile.java
+++ b/brut.j.dir/src/main/java/brut/directory/ExtFile.java
@@ -17,6 +17,7 @@
 package brut.directory;
 
 import java.io.File;
+import java.io.IOException;
 import java.net.URI;
 
 /**
@@ -54,5 +55,9 @@ public class ExtFile extends File {
         return mDirectory;
     }
 
+    public void close() throws IOException {
+        mDirectory.close();
+    }
+
     private Directory mDirectory;
 }
diff --git a/brut.j.dir/src/main/java/brut/directory/ZipRODirectory.java b/brut.j.dir/src/main/java/brut/directory/ZipRODirectory.java
index bb76298a..a400ce34 100644
--- a/brut.j.dir/src/main/java/brut/directory/ZipRODirectory.java
+++ b/brut.j.dir/src/main/java/brut/directory/ZipRODirectory.java
@@ -151,4 +151,8 @@ public class ZipRODirectory extends AbstractDirectory {
         return mZipFile;
     }
 
+
+    public void close() throws IOException {
+        mZipFile.close();
+    }
 }