com.nuvolect.deepdive.probe.DecompileApk.java Source code

Java tutorial

Introduction

Here is the source code for com.nuvolect.deepdive.probe.DecompileApk.java

Source

/*
 * Copyright (c) 2018 Nuvolect LLC.
 * This software is offered for free under conditions of the GPLv3 open source software license.
 * Contact Nuvolect LLC for a less restrictive commercial license if you would like to use the software
 * without the GPLv3 restrictions.
 */

package com.nuvolect.deepdive.probe;

import android.app.AlarmManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.preference.PreferenceManager;

import com.googlecode.dex2jar.Method;
import com.googlecode.dex2jar.ir.IrMethod;
import com.googlecode.dex2jar.reader.DexFileReader;
import com.googlecode.dex2jar.v3.Dex2jar;
import com.googlecode.dex2jar.v3.DexExceptionHandler;
import com.jaredrummler.apkparser.ApkParser;
import com.jaredrummler.apkparser.model.CertificateMeta;
import com.nuvolect.deepdive.main.App;
import com.nuvolect.deepdive.main.CConst;
import com.nuvolect.deepdive.util.LogUtil;
import com.nuvolect.deepdive.util.OmniFile;
import com.nuvolect.deepdive.util.OmniHash;
import com.nuvolect.deepdive.util.OmniUtil;
import com.nuvolect.deepdive.util.OmniZip;
import com.nuvolect.deepdive.util.TimeUtil;
import com.nuvolect.deepdive.util.Util;

import org.apache.commons.io.FilenameUtils;
import org.benf.cfr.reader.state.ClassFileSourceImpl;
import org.benf.cfr.reader.state.DCCommonState;
import org.benf.cfr.reader.util.MiscConstants;
import org.benf.cfr.reader.util.getopt.Options;
import org.benf.cfr.reader.util.getopt.OptionsImpl;
import org.benf.cfr.reader.util.output.DumperFactoryImpl;
import org.jetbrains.java.decompiler.main.decompiler.ConsoleDecompiler;
import org.jetbrains.java.decompiler.main.decompiler.PrintStreamLogger;
import org.jf.dexlib2.DexFileFactory;
import org.jf.dexlib2.Opcodes;
import org.jf.dexlib2.iface.ClassDef;
import org.jf.dexlib2.iface.DexFile;
import org.jf.dexlib2.immutable.ImmutableDexFile;
import org.jf.dexlib2.writer.pool.DexPool;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.objectweb.asm.tree.MethodNode;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintStream;
import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Scanner;
import java.util.Set;

import jadx.api.JadxDecompiler;

//import net.dongliu.apk.parser.ApkParser;
//import net.dongliu.apk.parser.bean.CertificateMeta;
//import net.dongliu.apk.parser.bean.DexClass;

/**
 * This class represents an object to work with a single APK file.
 * Each object is specific to a user and a package.
 * Multiple users can process multiple apps at the same time.
 * Sub-folders are created for each app named with the package name
 * Sub-folders contain
 * 1. the APK file,
 * 2. the unpacked APK file and
 * 3. decompiled class files in subfolders src{cfr, jadx, fern}
 *
 * Notes:
 * _status is for 0/1 apk and dex file exists status
 * _thread status has states {running, stopped, null}
 * running: compile process is running
 * stopped: comple process has stopped, folder exists
 * null: compile process is not running, folder does not exist
 *
 * Fernflower will only work with standard java File, i.e, only /sdcard or /private
 */
public class DecompileApk {

    private final Context m_ctx;
    private final String m_userId;
    private final String m_packageName;
    private final String m_volumeId;
    private final String m_userFolderPath;

    private OmniFile m_appFolder; // Includes package name for directory
    private OmniFile m_apkFile;
    private OmniFile m_dexFile;
    private OmniFile m_jarFile;
    private OmniFile m_optimizedDexLogFile;
    private OmniFile m_srcCfrFolder;
    private OmniFile m_srcFernFolder;
    private OmniFile m_srcJadxFolder;
    private ProgressStream m_progressStream;
    private String m_appApkPath;
    private String m_appFolderPath;
    private String m_appFolderUrl;
    private String m_srcCfrFolderPath;
    private String m_srcFernFolderPath;
    private String m_srcJadxFolderPath;

    private int STACK_SIZE = 20 * 1024 * 1024;

    private String DEEPDIVE_THREAD_GROUP = "DeepDive Thread Group";
    private ThreadGroup m_threadGroup = new ThreadGroup(DEEPDIVE_THREAD_GROUP);
    private Thread m_unpackApkThread = null;
    private Thread m_optimizeDexThread = null;
    private Thread m_dex2jarThread = null;
    private Thread m_cfrThread = null;
    private Thread m_jadxThread = null;
    private Thread m_fernThread = null;
    private String UNZIP_APK_THREAD = "Unpack APK java thread";
    private String DEX2JAR_THREAD = "DEX to JAR java thread";
    private String JADX_THREAD = "Jadx jar to java thread";
    private String FERN_THREAD = "Fernflower jar to java thread";
    private List<String> ignoredLibs = new ArrayList();
    private String OPTIMIZED_CLASSES = "optimized_classes";
    private String OPTIMIZED_CLASSES_EXCLUSION_FILENAME = "dex_class_exclusion.txt";
    private String README_FILENAME = "README.txt";
    private String DEX_OPTIMIZATION_LOG_FILE = "dex_optimization_log.txt";
    private String[] m_dexFileNames = {}; // Generated list of candidate dex file names

    // Time when a process is started
    private long m_unpack_apk_time = 0;
    private long m_dex2jar_time = 0;
    private long m_optimize_dex_time = 0;
    private long m_cfr_time = 0;
    private long m_jadx_time = 0;
    private long m_fern_time = 0;
    private int m_active_threads;

    public enum THREAD_ID {
        unpack_apk, dex2jar, cfr, jadx, fern_flower, optimize_dex
    };

    public DecompileApk(Context ctx, String userId, String packageName) {

        m_ctx = ctx;
        m_userId = userId;
        m_packageName = packageName;
        m_userFolderPath = App.getUser().getUserFolderPath();
        m_volumeId = App.getUser().getDefaultVolumeId();

        m_progressStream = new ProgressStream();
        m_progressStream.putStream("Decompiler ready");

        /*
         * Generate a list of candidate dex file names
         * {classes, classes2..classes64}
         */
        List<String> tmp = new ArrayList<String>();
        tmp.add("classes");
        for (int i = 2; i <= 64; i++)
            tmp.add("classes" + i);

        m_dexFileNames = new String[tmp.size()];
        m_dexFileNames = tmp.toArray(m_dexFileNames);

        m_appFolderPath = (CConst.USER_FOLDER_PATH + m_packageName + "/").replace("//", "/");
        m_appFolder = new OmniFile(m_volumeId, m_appFolderPath);
        if (!m_appFolder.exists()) {
            m_appFolder.mkdirs();
        }
        m_appFolderUrl = OmniHash.getStartPathUrl(m_ctx, m_volumeId, m_appFolderPath);

        m_appApkPath = m_appFolderPath + m_packageName + ".apk";
        m_apkFile = new OmniFile(m_volumeId, m_appApkPath);
        m_dexFile = new OmniFile(m_volumeId, m_appFolderPath + "classes.dex");
        m_optimizedDexLogFile = new OmniFile(m_volumeId, m_appFolderPath + DEX_OPTIMIZATION_LOG_FILE);
        m_jarFile = new OmniFile(m_volumeId, m_appFolderPath + "classes.jar");
        m_srcCfrFolderPath = m_appFolderPath + "srcCfr";
        m_srcCfrFolder = new OmniFile(m_volumeId, m_srcCfrFolderPath);
        m_srcJadxFolderPath = m_appFolderPath + "srcJadx";
        m_srcJadxFolder = new OmniFile(m_volumeId, m_srcJadxFolderPath);
        m_srcFernFolderPath = m_appFolderPath + "srcFern";
        m_srcFernFolder = new OmniFile(m_volumeId, m_srcFernFolderPath);
    }

    /**
     * Update member variables paths, and files. Return thread status and URLs.
     * @return
     */
    public JSONObject getStatus() {

        /**
         * Update status on tasks performed
         */
        boolean apkFileExists = m_apkFile.exists();
        boolean dexFileExists = m_dexFile.exists();
        boolean optimizedDexExists = m_optimizedDexLogFile.exists();
        boolean jarFileExists = m_jarFile.exists();
        boolean cfrFolderExists = m_srcCfrFolder.exists();
        boolean jadxFolderExists = m_srcJadxFolder.exists();
        boolean fernFolderExists = m_srcFernFolder.exists();
        JSONObject wrapper = new JSONObject();

        try {

            wrapper.put("extract_apk_status", apkFileExists ? 1 : 0);
            wrapper.put("app_folder_url", m_appFolderUrl);
            wrapper.put("app_folder_path", m_appFolderPath);

            if (cfrFolderExists) {
                String url = OmniHash.getStartPathUrl(m_ctx, m_volumeId, m_srcCfrFolderPath);
                wrapper.put("cfr_url", url);
            } else {
                wrapper.put("cfr_url", m_appFolderUrl);
            }

            if (jadxFolderExists) {
                String url = OmniHash.getStartPathUrl(m_ctx, m_volumeId, m_srcJadxFolderPath);
                wrapper.put("jadx_url", url);
            } else {
                wrapper.put("jadx_url", m_appFolderUrl);
            }

            if (fernFolderExists) {
                String url = OmniHash.getStartPathUrl(m_ctx, m_volumeId, m_srcFernFolderPath);
                wrapper.put("fern_url", url);
            } else {
                wrapper.put("fern_url", m_appFolderUrl);
            }

            wrapper.put("optimize_dex_status", optimizedDexExists ? 1 : 0);

            m_active_threads = 0;
            wrapper.put("unpack_apk_thread", getThreadStatus(dexFileExists, m_unpackApkThread));
            wrapper.put("dex2jar_thread", getThreadStatus(jarFileExists, m_dex2jarThread));
            wrapper.put("optimize_dex_thread", getThreadStatus(optimizedDexExists, m_optimizeDexThread));
            wrapper.put("cfr_thread", getThreadStatus(cfrFolderExists, m_cfrThread));
            wrapper.put("jadx_thread", getThreadStatus(jadxFolderExists, m_jadxThread));
            wrapper.put("fern_thread", getThreadStatus(fernFolderExists, m_fernThread));
            wrapper.put("active_threads", m_active_threads);

            wrapper.put("unpack_apk_time", getThreadTime(m_unpack_apk_time));
            wrapper.put("dex2jar_time", getThreadTime(m_dex2jar_time));
            wrapper.put("optimize_dex_time", getThreadTime(m_optimize_dex_time));
            wrapper.put("cfr_time", getThreadTime(m_cfr_time));
            wrapper.put("jadx_time", getThreadTime(m_jadx_time));
            wrapper.put("fern_time", getThreadTime(m_fern_time));

            wrapper.put("upload_url", "/probe/upload_apk");
            wrapper.put("log", getStream());

        } catch (JSONException e) {
            e.printStackTrace();
        }

        return wrapper;
    }

    /**
     * Dispatch to perform the assigned action
     * @param action
     * @return
     */
    public JSONObject startThread(Context ctx, String action) {

        SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(ctx);
        STACK_SIZE = Integer.valueOf(prefs.getString(CConst.THREAD_STACK_SIZE, "20")) * 1024 * 1024;

        THREAD_ID action_id = null;
        try {
            action_id = THREAD_ID.valueOf(action);
        } catch (IllegalArgumentException e) {
            LogUtil.log(LogUtil.LogType.DECOMPILE, "Error, invalid command: " + action);
        }

        switch (action_id) {

        case unpack_apk:
            return unpackApk();
        case optimize_dex:
            return optimizeDex();
        case dex2jar:
            return dex2jar();
        case cfr:
            return cfr();
        case jadx:
            return jadx();
        case fern_flower:
            return fern_flower();
        default:
            return null;
        }
    }

    /**
     * Return the status of a compile process. The status can be one of three states:
     * running: compile process is running
     * stopped: comple process has stopped, folder exists
     * empty: compile process is not running, folder does not exist
     *
     * @param folderExists
     * @param aThread
     * @return
     *
     * The member variable {@link #m_active_threads} is bumped for each active thread
     */
    private String getThreadStatus(boolean folderExists, Thread aThread) {

        if (aThread != null && aThread.isAlive()) {

            ++m_active_threads;
            return "running";
        }

        if (folderExists)
            return "stopped";

        return "empty";
    }

    private String getThreadTime(long startTime) {

        if (startTime == 0)
            return "";

        return TimeUtil.deltaTimeHrMinSec(startTime);
    }

    /**
     * Copy the specific APK to working folder.
     * Return a link to the parent folder.
     * @return
     */
    public JSONObject extractApk() {

        JSONObject wrapper = new JSONObject();

        try {
            wrapper.put("extract_apk_status", 0);// 0==Start with failed file copy
            m_progressStream.putStream("Extract APK starting");

            PackageManager pm = m_ctx.getPackageManager();
            ApplicationInfo applicationInfo = pm.getApplicationInfo(m_packageName, PackageManager.GET_META_DATA);

            java.io.File inputFile = new File(applicationInfo.publicSourceDir);
            InputStream inputStream = new FileInputStream(inputFile);

            OutputStream outputStream = m_apkFile.getOutputStream();
            int bytes_copied = Util.copyFile(inputStream, outputStream);
            String formatted_count = NumberFormat.getNumberInstance(Locale.US).format(bytes_copied);

            m_progressStream.putStream("Extract APK complete, " + formatted_count + " bytes");

            wrapper.put("extract_apk_status", 1); // Change to success if we get here
            wrapper.put("extract_apk_url", m_appFolderUrl);

        } catch (PackageManager.NameNotFoundException | JSONException | IOException e) {
            LogUtil.logException(LogUtil.LogType.DECOMPILE, e);
            m_progressStream.putStream(e.toString());
            m_progressStream.putStream("Extract APK failed");
        }

        return wrapper;
    }

    private JSONObject unpackApk() {

        final Thread.UncaughtExceptionHandler uncaughtExceptionHandler = new Thread.UncaughtExceptionHandler() {
            @Override
            public void uncaughtException(Thread t, Throwable e) {

                LogUtil.log(LogUtil.LogType.DECOMPILE, "Uncaught exception: " + e.toString());
                m_progressStream.putStream("Uncaught exception: " + t.getName());
                m_progressStream.putStream("Uncaught exception: " + e.toString());
            }
        };

        m_unpack_apk_time = System.currentTimeMillis(); // Save start time for tracking

        m_unpackApkThread = new Thread(m_threadGroup, new Runnable() {
            @Override
            public void run() {
                boolean success = false;
                try {

                    m_progressStream = new ProgressStream(
                            new OmniFile(m_volumeId, m_appFolderPath + "unpack_apk_log.txt"));
                    m_progressStream.putStream("Unpack APK starting");
                    if (m_apkFile.exists() && m_apkFile.isFile()) {

                        // Extract all files except for XML, to be extracted later
                        success = ApkZipUtil.unzipAllExceptXML(m_apkFile, m_appFolder, m_progressStream);

                        ApkParser apkParser = ApkParser.create(m_apkFile.getStdFile());
                        ArrayList<OmniFile> dexFiles = new ArrayList<>();

                        // Get a list of all files in the APK and iterate and extract by type
                        List<String> paths = OmniZip.getFilesList(m_apkFile);
                        for (String path : paths) {

                            OmniFile file = new OmniFile(m_volumeId, m_appFolderPath + path);
                            OmniUtil.forceMkdirParent(file);

                            String extension = FilenameUtils.getExtension(path);

                            if (extension.contentEquals("xml")) {

                                String xml = apkParser.transBinaryXml(path);
                                OmniUtil.writeFile(file, xml);
                                m_progressStream.putStream("Translated: " + path);
                            }
                            if (extension.contentEquals("dex")) {
                                dexFiles.add(file);
                            }
                        }
                        paths = null; // Release memory

                        // Write over manifest with unencoded version
                        String manifestXml = apkParser.getManifestXml();
                        OmniFile manifestFile = new OmniFile(m_volumeId, m_appFolderPath + "AndroidManifest.xml");
                        OmniUtil.writeFile(manifestFile, manifestXml);
                        m_progressStream.putStream("Translated and parsed: " + "AndroidManifest.xml");

                        // Uses original author CaoQianLi's apk-parser
                        // compile 'net.dongliu:apk-parser:2.1.7'
                        //                        for( CertificateMeta cm : apkParser.getCertificateMetaList()){
                        //
                        //                            m_progressStream.putStream("Certficate base64 MD5: "+cm.getCertBase64Md5());
                        //                            m_progressStream.putStream("Certficate MD5: "+cm.getCertMd5());
                        //                            m_progressStream.putStream("Sign algorithm OID: "+cm.getSignAlgorithmOID());
                        //                            m_progressStream.putStream("Sign algorithm: "+cm.getSignAlgorithm());
                        //                        }

                        for (OmniFile f : dexFiles) {

                            String formatted_count = String.format(Locale.US, "%,d", f.length()) + " bytes";
                            m_progressStream.putStream("DEX extracted: " + f.getName() + ": " + formatted_count);
                        }
                        dexFiles = new ArrayList<>();// Release memory

                        CertificateMeta cm = null;
                        try {
                            cm = apkParser.getCertificateMeta();
                            m_progressStream.putStream("Certficate base64 MD5: " + cm.certBase64Md5);
                            m_progressStream.putStream("Certficate MD5: " + cm.certMd5);
                            m_progressStream.putStream("Sign algorithm OID: " + cm.signAlgorithmOID);
                            m_progressStream.putStream("Sign algorithm: " + cm.signAlgorithm);

                        } catch (Exception e1) {
                            e1.printStackTrace();
                        }

                        m_progressStream.putStream("ApkSignStatus: " + apkParser.verifyApk());

                        /**
                         * Create a file for the user to include classes to omit in the optimize DEX task.
                         */
                        OmniFile optimizedDexOF = new OmniFile(m_volumeId,
                                m_appFolderPath + OPTIMIZED_CLASSES_EXCLUSION_FILENAME);
                        if (!optimizedDexOF.exists()) {

                            String assetFilePath = CConst.ASSET_DATA_FOLDER + OPTIMIZED_CLASSES_EXCLUSION_FILENAME;
                            OmniFile omniFile = new OmniFile(m_volumeId,
                                    m_appFolderPath + OPTIMIZED_CLASSES_EXCLUSION_FILENAME);
                            OmniUtil.copyAsset(m_ctx, assetFilePath, omniFile);

                            m_progressStream.putStream("File created: " + OPTIMIZED_CLASSES_EXCLUSION_FILENAME);
                        }
                        /**
                         * Create a README file for the user.
                         */
                        OmniFile README_file = new OmniFile(m_volumeId, m_appFolderPath + README_FILENAME);
                        if (!README_file.exists()) {

                            String assetFilePath = CConst.ASSET_DATA_FOLDER + README_FILENAME;
                            OmniFile omniFile = new OmniFile(m_volumeId, m_appFolderPath + README_FILENAME);
                            OmniUtil.copyAsset(m_ctx, assetFilePath, omniFile);

                            m_progressStream.putStream("File created: " + README_FILENAME);
                        }
                    } else {
                        m_progressStream.putStream("APK not found. Select Extract APK.");
                    }

                } catch (Exception | StackOverflowError e) {
                    m_progressStream.putStream(e.toString());
                }
                String time = TimeUtil.deltaTimeHrMinSec(m_unpack_apk_time);
                m_unpack_apk_time = 0;
                if (success) {
                    m_progressStream.putStream("Unpack APK complete: " + time);
                } else {
                    m_progressStream.putStream("Unpack APK failed: " + time);
                }
                m_progressStream.close();

            }
        }, UNZIP_APK_THREAD, STACK_SIZE);

        m_unpackApkThread.setPriority(Thread.MAX_PRIORITY);
        m_unpackApkThread.setUncaughtExceptionHandler(uncaughtExceptionHandler);
        m_unpackApkThread.start();

        final JSONObject wrapper = new JSONObject();
        try {
            wrapper.put("unpack_apk_thread", getThreadStatus(true, m_unpackApkThread));

        } catch (JSONException e) {
            LogUtil.logException(LogUtil.LogType.DECOMPILE, e);
        }

        return wrapper;
    }

    /**
     * Build a new DEX file excluding classes in the OPTIMIZED_CLASS_EXCLUSION file
     * @return
     */
    private JSONObject optimizeDex() {

        final Thread.UncaughtExceptionHandler uncaughtExceptionHandler = new Thread.UncaughtExceptionHandler() {
            @Override
            public void uncaughtException(Thread t, Throwable e) {

                LogUtil.log(LogUtil.LogType.DECOMPILE, "Uncaught exception: " + e.toString());
                m_progressStream.putStream("Uncaught exception: " + t.getName());
                m_progressStream.putStream("Uncaught exception: " + e.toString());
            }
        };

        m_optimize_dex_time = System.currentTimeMillis(); // Save start time for tracking

        m_optimizeDexThread = new Thread(m_threadGroup, new Runnable() {
            @Override
            public void run() {

                m_progressStream = new ProgressStream(
                        new OmniFile(m_volumeId, m_appFolderPath + DEX_OPTIMIZATION_LOG_FILE));

                m_progressStream
                        .putStream("Optimizing classes, reference: " + OPTIMIZED_CLASSES_EXCLUSION_FILENAME);

                Scanner s = null;
                try {
                    OmniFile omniFile = new OmniFile(m_volumeId,
                            m_appFolderPath + OPTIMIZED_CLASSES_EXCLUSION_FILENAME);
                    s = new Scanner(omniFile.getStdFile());
                    while (s.hasNext()) {
                        String excludeClass = s.next();
                        ignoredLibs.add(excludeClass);
                        m_progressStream.putStream("Exclude class: " + excludeClass);
                    }
                } catch (Exception e) {
                    LogUtil.logException(LogUtil.LogType.DECOMPILE, e);
                }
                if (s != null)
                    s.close();

                ArrayList<OmniFile> dexFiles = new ArrayList<>();

                for (String fileName : m_dexFileNames) {

                    OmniFile dexFile = new OmniFile(m_volumeId, m_appFolderPath + fileName + ".dex");

                    if (dexFile.exists() && dexFile.isFile()) {

                        dexFiles.add(dexFile);// Keep track for summary

                        List<ClassDef> classes = new ArrayList<>();
                        m_progressStream.putStream("Processing: " + fileName + ".dex");
                        org.jf.dexlib2.iface.DexFile memoryDexFile = null;
                        try {
                            memoryDexFile = DexFileFactory.loadDexFile(dexFile.getStdFile(), Opcodes.forApi(19));
                        } catch (Exception e) {
                            m_progressStream.putStream("The app DEX file cannot be decompiled.");
                            LogUtil.logException(LogUtil.LogType.DECOMPILE, e);
                            continue;
                        }

                        int excludedClassCount = 0;
                        Set<? extends ClassDef> origClassSet = memoryDexFile.getClasses();
                        memoryDexFile = null; // Release memory

                        for (org.jf.dexlib2.iface.ClassDef classDef : origClassSet) {

                            final String currentClass = classDef.getType();

                            if (isIgnored(currentClass)) {
                                ++excludedClassCount;
                                m_progressStream.putStream("Excluded class: " + currentClass);
                            } else {
                                m_progressStream.putStream("Included class: " + currentClass);
                                classes.add(classDef);
                            }
                        }
                        origClassSet = null; // Release memory

                        m_progressStream.putStream("Excluded classes #" + excludedClassCount);
                        m_progressStream.putStream("Included classes #" + classes.size());
                        m_progressStream.putStream("Rebuilding immutable dex: " + fileName + ".dex");

                        if (classes.size() > 0) {

                            DexFile optDexFile = new ImmutableDexFile(Opcodes.forApi(19), classes);
                            classes = null; // Release memory

                            try {
                                if (dexFile.delete())
                                    m_progressStream.putStream("Fat DEX file delete success: " + dexFile.getName());
                                else
                                    m_progressStream.putStream("Fat DEX file delete FAILED: " + dexFile.getName());

                                DexPool.writeTo(dexFile.getStdFile().getAbsolutePath(), optDexFile);
                                String size = NumberFormat.getNumberInstance(Locale.US).format(dexFile.length());

                                m_progressStream.putStream(
                                        "Optimized DEX file created: " + dexFile.getName() + ", size: " + size);
                            } catch (IOException e) {
                                m_progressStream.putStream("DEX IOException, write error: " + dexFile.getName());
                                LogUtil.logException(LogUtil.LogType.DECOMPILE, e);
                            } catch (Exception e) {
                                m_progressStream.putStream("DEX Exception, write error: " + dexFile.getName());
                                LogUtil.logException(LogUtil.LogType.DECOMPILE, e);
                            }
                            optDexFile = null; // release memory
                        } else {

                            m_progressStream
                                    .putStream("All classes excluded, DEX file not needed: " + dexFile.getName());
                            m_progressStream.putStream("Deleting: " + dexFile.getName());
                            dexFile.delete();
                        }
                    }
                }
                for (OmniFile f : dexFiles) {

                    if (f.exists()) {

                        String formatted_count = String.format(Locale.US, "%,d", f.length()) + " bytes";
                        m_progressStream.putStream("DEX optimized: " + f.getName() + ": " + formatted_count);
                    } else {
                        m_progressStream.putStream("DEX deleted: " + f.getName() + ", all classes excluded");
                    }
                }
                dexFiles = new ArrayList<>();// Release memory

                m_progressStream
                        .putStream("Optimize DEX complete: " + TimeUtil.deltaTimeHrMinSec(m_optimize_dex_time));
                m_progressStream.close();
                m_optimize_dex_time = 0;

            }
        }, UNZIP_APK_THREAD, STACK_SIZE);

        m_optimizeDexThread.setPriority(Thread.MAX_PRIORITY);
        m_optimizeDexThread.setUncaughtExceptionHandler(uncaughtExceptionHandler);
        m_optimizeDexThread.start();

        return new JSONObject();
    }

    private boolean isIgnored(String className) {

        for (String ignoredClass : ignoredLibs) {

            if (className.startsWith(ignoredClass)) {
                return true;
            }
        }
        return false;
    }

    private JSONObject dex2jar() {

        // DEX 2 JAR CONFIGS
        final boolean reuseReg = false; // reuse register while generate java .class file
        final boolean topologicalSort1 = false; // same with --topological-sort/-ts
        final boolean topologicalSort = false; // sort block by topological, that will generate more readable code
        final boolean verbose = true; // show progress
        final boolean debugInfo = false; // translate debug info
        final boolean printIR = false; // print ir to System.out
        final boolean optimizeSynchronized = true; // Optimise-synchronised

        final Thread.UncaughtExceptionHandler uncaughtExceptionHandler = new Thread.UncaughtExceptionHandler() {
            @Override
            public void uncaughtException(Thread t, Throwable e) {

                LogUtil.log(LogUtil.LogType.DECOMPILE, "Uncaught exception: " + e.toString());
                m_progressStream.putStream("Uncaught exception: " + t.getName());
                m_progressStream.putStream("Uncaught exception: " + e.toString());
            }
        };

        m_dex2jar_time = System.currentTimeMillis(); // Save start time for tracking

        m_dex2jarThread = new Thread(m_threadGroup, new Runnable() {
            @Override
            public void run() {

                boolean success = false;
                OmniFile dexFile = null;
                OmniFile jarFile = null;
                m_progressStream.putStream("DEX to JAR starting");

                for (String fileName : m_dexFileNames) {

                    dexFile = new OmniFile(m_volumeId, m_appFolderPath + fileName + ".dex");

                    if (dexFile.exists() && dexFile.isFile()) {

                        String size = NumberFormat.getNumberInstance(Locale.US).format(dexFile.length());
                        m_progressStream.putStream("DEX to JAR processing: " + dexFile.getName() + ", " + size);

                        DexExceptionHandlerMod dexExceptionHandlerMod = new DexExceptionHandlerMod();
                        jarFile = new OmniFile(m_volumeId, m_appFolderPath + fileName + ".jar");

                        if (jarFile.exists())
                            jarFile.delete();

                        try {
                            DexFileReader reader = new DexFileReader(dexFile.getStdFile());
                            Dex2jar dex2jar = Dex2jar.from(reader).reUseReg(reuseReg)
                                    .topoLogicalSort(topologicalSort || topologicalSort1).skipDebug(!debugInfo)
                                    .optimizeSynchronized(optimizeSynchronized).printIR(printIR);
                            //.verbose(verbose);
                            dex2jar.setExceptionHandler(dexExceptionHandlerMod);
                            dex2jar.to(jarFile.getStdFile());
                            success = true;
                        } catch (Exception e) {
                            String ex = LogUtil.logException(LogUtil.LogType.DECOMPILE, e);
                            m_progressStream.putStream(ex);
                            m_progressStream.putStream("DEX to JAR failed: " + jarFile.getName());
                            success = false;
                            continue;
                        }
                        if (success) {
                            size = NumberFormat.getNumberInstance(Locale.US).format(jarFile.length());
                            m_progressStream.putStream("DEX to JAR succeeded: " + jarFile.getName() + ", " + size);
                        } else {
                            m_progressStream
                                    .putStream("Exception thrown, file cannot be decompiled: " + dexFile.getPath());
                        }
                    }
                }
                if (jarFile == null)
                    m_progressStream.putStream("No DEX file found: " + m_dexFileNames);

                m_progressStream.putStream("DEX to JAR complete: " + TimeUtil.deltaTimeHrMinSec(m_dex2jar_time));
                m_dex2jar_time = 0;

            }

        }, DEX2JAR_THREAD, STACK_SIZE);

        m_dex2jarThread.setPriority(Thread.MAX_PRIORITY);
        m_dex2jarThread.setUncaughtExceptionHandler(uncaughtExceptionHandler);
        m_dex2jarThread.start();

        JSONObject wrapper = new JSONObject();
        try {
            wrapper.put("dex2jar_thread", getThreadStatus(true, m_dex2jarThread));

        } catch (JSONException e) {
            LogUtil.logException(LogUtil.LogType.DECOMPILE, e);
        }

        return wrapper;
    }

    private JSONObject cfr() {

        m_srcCfrFolder.mkdirs();

        Thread.UncaughtExceptionHandler uncaughtExceptionHandler = new Thread.UncaughtExceptionHandler() {
            @Override
            public void uncaughtException(Thread t, Throwable e) {

                LogUtil.log(LogUtil.LogType.DECOMPILE, "Uncaught exception: " + e.toString());
                m_progressStream.putStream("Uncaught exception: " + t.getName());
                m_progressStream.putStream("Uncaught exception: " + e.toString());
            }
        };

        m_cfr_time = System.currentTimeMillis(); // Save start time for tracking

        m_cfrThread = new Thread(m_threadGroup, new Runnable() {
            @Override
            public void run() {

                m_progressStream = new ProgressStream(
                        new OmniFile(m_volumeId, m_srcCfrFolderPath + "cfr_decompile_log.txt"));
                m_progressStream.putStream("CFR " + MiscConstants.CFR_VERSION + " starting");
                OmniFile jarFile = null;
                try {
                    for (String fileName : m_dexFileNames) {

                        jarFile = new OmniFile(m_volumeId, m_appFolderPath + fileName + ".jar");

                        if (jarFile.exists() && jarFile.isFile()) {

                            String[] args = { jarFile.getStdFile().toString(), "--outputdir",
                                    m_srcCfrFolder.getStdFile().toString() };

                            Map<String, String> optionArgs = new HashMap<String, String>();
                            optionArgs.put("outputdir", m_srcCfrFolder.getStdFile().toString());
                            Options options = new OptionsImpl(optionArgs);
                            ClassFileSourceImpl classFileSource = new ClassFileSourceImpl(options);
                            final DCCommonState dcCommonState = new DCCommonState(options, classFileSource);
                            DumperFactoryImpl dumperFactory = new DumperFactoryImpl(options);
                            org.benf.cfr.reader.Main.doJar(dcCommonState, jarFile.getAbsolutePath(), dumperFactory);
                        }
                    }

                } catch (Exception | StackOverflowError e) {
                    m_progressStream.putStream(e.toString());
                }
                m_progressStream.putStream("CFR complete: " + TimeUtil.deltaTimeHrMinSec(m_cfr_time));
                m_progressStream.close();
                m_cfr_time = 0;

            }
        }, DEEPDIVE_THREAD_GROUP, STACK_SIZE);

        m_cfrThread.setPriority(Thread.MAX_PRIORITY);
        m_cfrThread.setUncaughtExceptionHandler(uncaughtExceptionHandler);
        m_cfrThread.start();

        String processKey = "cfr_thread";
        String urlKey = "cfr_url";

        return processWrapper(processKey, urlKey);
    }

    /**
     * Jadx converts a DEX file directly into Java files.  It does not input JAR files.
     */
    private JSONObject jadx() {

        m_srcJadxFolder.mkdirs();
        Thread.UncaughtExceptionHandler uncaughtExceptionHandler = new Thread.UncaughtExceptionHandler() {
            @Override
            public void uncaughtException(Thread t, Throwable e) {

                LogUtil.log(LogUtil.LogType.DECOMPILE, "Uncaught exception: " + e.toString());
                m_progressStream.putStream("Uncaught exception: " + t.getName());
                m_progressStream.putStream("Uncaught exception: " + e.toString());
            }
        };

        m_jadx_time = System.currentTimeMillis(); // Save start time for tracking

        m_jadxThread = new Thread(m_threadGroup, new Runnable() {
            @Override
            public void run() {

                m_progressStream.putStream("Jadx starting");
                /*
                 * Type File require, versus OmniFile, in order to provide loadFiles
                 * a list of <File>.
                 */
                List<File> dexList = new ArrayList<>();
                JadxDecompiler jadx = new JadxDecompiler();
                jadx.setOutputDir(m_srcJadxFolder.getStdFile());
                String loadingNames = "";
                String spacer = "";

                for (String fileName : m_dexFileNames) {

                    OmniFile dexFile = new OmniFile(m_volumeId, m_appFolderPath + fileName + ".dex");

                    if (dexFile.exists() && dexFile.isFile()) {

                        dexList.add(dexFile.getStdFile());
                        loadingNames += spacer + dexFile.getName();
                        spacer = ", ";

                        if (fileName.contentEquals(OPTIMIZED_CLASSES))
                            break;
                    }
                }
                try {
                    m_progressStream.putStream("Loading: " + loadingNames);
                    jadx.loadFiles(dexList);
                    m_progressStream.putStream("Load complete");
                } catch (Exception e) {
                    LogUtil.logException(LogUtil.LogType.DECOMPILE, e);
                    m_progressStream.putStream(e.toString());
                }
                try {
                    m_progressStream.putStream("Jadx saveSources start");
                    jadx.saveSources();
                    m_progressStream.putStream("Jadx saveSources complete");
                } catch (Exception e) {
                    LogUtil.logException(LogUtil.LogType.DECOMPILE, e);
                    m_progressStream.putStream(e.toString());
                }

                m_progressStream.putStream("Jadx complete: " + TimeUtil.deltaTimeHrMinSec(m_jadx_time));
                m_jadx_time = 0;

            }
        }, JADX_THREAD, STACK_SIZE);

        m_jadxThread.setPriority(Thread.MAX_PRIORITY);
        m_jadxThread.setUncaughtExceptionHandler(uncaughtExceptionHandler);
        m_jadxThread.start();

        String processKey = "jadx_thread";
        //                processStatus = getThreadStatus( true, m_jadxThread);
        String urlKey = "jadx_url";
        //                url = OmniHash.getHashedServerUrl( m_ctx, m_volumeId, m_srcJadxFolderPath);

        return processWrapper(processKey, urlKey);
    }

    /**
     * Fernflower converts JAR files to a zipped decompiled JAR file then
     * it unzips the JAR file.
     */
    private JSONObject fern_flower() {// https://github.com/fesh0r/fernflower

        m_srcFernFolder.mkdirs();

        Thread.UncaughtExceptionHandler uncaughtExceptionHandler = new Thread.UncaughtExceptionHandler() {
            @Override
            public void uncaughtException(Thread t, Throwable e) {

                LogUtil.log(LogUtil.LogType.DECOMPILE, "Uncaught exception: " + e.toString());
                m_progressStream.putStream("Uncaught exception: " + t.getName());
                m_progressStream.putStream("Uncaught exception: " + e.toString());
            }
        };

        m_fern_time = System.currentTimeMillis(); // Save start time for tracking

        m_fernThread = new Thread(m_threadGroup, new Runnable() {
            @Override
            public void run() {

                File inputJarFile = null;
                String inputJarFileName = "";

                for (int i = 0; i < m_dexFileNames.length; i++) {

                    inputJarFileName = m_dexFileNames[i] + ".jar";
                    OmniFile inputJarOmniFile = new OmniFile(m_volumeId, m_appFolderPath + inputJarFileName);
                    inputJarFile = inputJarOmniFile.getStdFile();

                    if (inputJarFile.exists() && inputJarFile.isFile()) {

                        boolean success = true;
                        try {
                            m_progressStream.putStream("Fernflower starting: " + inputJarFileName);
                            PrintStream printStream = new PrintStream(m_progressStream);
                            System.setErr(printStream);
                            System.setOut(printStream);
                            OmniFile fernLog = new OmniFile(m_volumeId,
                                    m_srcFernFolderPath + "/" + m_dexFileNames[i] + "_log.txt");
                            PrintStream logStream = new PrintStream(fernLog.getOutputStream());
                            PrintStreamLogger logger = new PrintStreamLogger(logStream);

                            final Map<String, Object> mapOptions = new HashMap<>();
                            ConsoleDecompiler decompiler = new ConsoleDecompiler(m_srcFernFolder.getStdFile(),
                                    mapOptions, logger);
                            decompiler.addSpace(inputJarFile, true);

                            m_progressStream
                                    .putStream("Fernflower decompiler.addSpace complete: " + inputJarFileName);
                            decompiler.decompileContext();
                            m_progressStream.putStream(
                                    "Fernflower decompiler.decompileContext complete: " + inputJarFileName);

                            String decompiledJarFilePath = m_srcFernFolderPath + "/" + inputJarFileName;
                            OmniFile decompiledJarFile = new OmniFile(m_volumeId, decompiledJarFilePath);
                            success = OmniZip.unzipFile(decompiledJarFile, m_srcFernFolder, null, null);
                            decompiledJarFile.delete();

                            if (success) {
                                m_progressStream
                                        .putStream("Fernflower decompiler.unpack complete: " + inputJarFileName);
                            } else {
                                m_progressStream
                                        .putStream("Fernflower decompiler.unpack failed: " + inputJarFileName);
                            }
                        } catch (Exception e) {
                            String str = LogUtil.logException(LogUtil.LogType.FERNFLOWER, e);
                            m_progressStream.putStream("Fernflower exception " + inputJarFileName);
                            m_progressStream.putStream(str);
                            success = false;
                        }
                        /**
                         * Look for the classes.jar file and unzip it
                         */
                        if (!success) {

                            OmniFile of = new OmniFile(m_volumeId, m_srcFernFolderPath + "/classes.jar");
                            if (of.exists()) {

                                ApkZipUtil.unzip(of, m_srcFernFolder, m_progressStream);
                                m_progressStream.putStream(
                                        "Fernflower utility unzip complete with errors: " + inputJarFileName);
                            } else {
                                m_progressStream.putStream("File does not exist: " + of.getAbsolutePath());
                            }
                        }
                    }
                }
                m_progressStream.putStream("Fernflower complete: " + TimeUtil.deltaTimeHrMinSec(m_fern_time));
                m_fern_time = 0;

            }
        }, FERN_THREAD, STACK_SIZE);

        m_fernThread.setPriority(Thread.MAX_PRIORITY);
        m_fernThread.setUncaughtExceptionHandler(uncaughtExceptionHandler);
        m_fernThread.start();

        //                String processStatus = getThreadStatus( true, m_fernThread);
        //                String url = OmniHash.getHashedServerUrl( m_ctx, m_volumeId, m_srcFernFolderPath);

        String processKey = "fern_thread";
        String urlKey = "fern_url";

        return processWrapper(processKey, urlKey);
    }

    private JSONObject processWrapper(String processKey, String urlKey) {

        JSONObject wrapper = new JSONObject();

        try {
            wrapper.put("url", m_appFolderUrl);
            wrapper.put(processKey, null);
            wrapper.put(urlKey, "");

        } catch (JSONException e) {
            e.printStackTrace();
        }

        return wrapper;
    }

    public JSONObject stopThread(String threadId) {

        THREAD_ID thread = THREAD_ID.valueOf(threadId);
        Thread myThread = null;

        switch (thread) {

        case unpack_apk:
            myThread = m_unpackApkThread;
            break;
        case dex2jar:
            myThread = m_dex2jarThread;
            break;
        case optimize_dex:
            myThread = m_optimizeDexThread;
            break;
        case cfr:
            myThread = m_cfrThread;
            break;
        case jadx:
            myThread = m_jadxThread;
            break;
        case fern_flower:
            myThread = m_fernThread;
            break;
        default:// do nothing
        }

        //        if( myThread != null){
        //                myThread.currentThread().stop();// deprecated as of API 16 jellybean
        //            myThread.currentThread().interrupt(); // not working, does not stop thread
        //        }

        /**
         * Not the best solution but attempts to selectively stop individual threads do not
         * seem to work. We need need a more robust solution for long running process management.
         */
        Intent mStartActivity = new Intent(m_ctx, com.nuvolect.deepdive.main.MainActivity.class);
        int mPendingIntentId = 123456;
        PendingIntent mPendingIntent = PendingIntent.getActivity(m_ctx, mPendingIntentId, mStartActivity,
                PendingIntent.FLAG_CANCEL_CURRENT);
        AlarmManager mgr = (AlarmManager) m_ctx.getSystemService(Context.ALARM_SERVICE);
        mgr.set(AlarmManager.RTC, System.currentTimeMillis() + 100, mPendingIntent);
        System.exit(0);

        Runtime.getRuntime().exit(0);

        return getStatus();
    }

    private class DexExceptionHandlerMod implements DexExceptionHandler {
        @Override
        public void handleFileException(Exception e) {
            LogUtil.logException(LogUtil.LogType.DECOMPILE, "Dex2Jar Exception", e);
        }

        @Override
        public void handleMethodTranslateException(Method method, IrMethod irMethod, MethodNode methodNode,
                Exception e) {
            LogUtil.logException(LogUtil.LogType.DECOMPILE, "Dex2Jar Exception", e);
        }
    }

    public void clearStream() {

        m_progressStream.init();
    }

    public JSONArray getStream() {

        return m_progressStream.getStream();
    }

}