All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.gitee.l0km.casban.AndroidPackageScanner Maven / Gradle / Ivy

There is a newer version: 1.4.1
Show newest version
package com.gitee.l0km.casban;

import com.gitee.l0km.casban.utils.ZipUtil;
import com.google.common.base.Predicate;
import com.google.common.base.Strings;
import com.google.common.collect.FluentIterable;

import dalvik.system.DexFile;
import net.gdface.utils.SimpleLog;
import org.w3c.dom.*;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;

import static com.google.common.base.Preconditions.checkNotNull;

import java.io.*;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.*;
import java.util.zip.ZipEntry;
import java.util.zip.ZipException;
import java.util.zip.ZipFile;
import java.util.zip.ZipOutputStream;

public class AndroidPackageScanner extends PackageScanner{
    public static final AndroidPackageScanner ANDROID_PACKAGE_SCANNER = new AndroidPackageScanner();
    /** 应用包名 */
    private String appPackageName;
    /** 应用安装包路径 */
    private String apkSourceDir;
    /** XML信息存储文档路径 */
    private String sharedPrefsXmlPath;
    /** DEX文件存储路径 */
    private String dexDir;

    /**
     * 扫描指定包名下的所有类
     * @param packagename
     * @throws IOException
     */
    @Override
    protected LinkedHashSet> getClasses(String packagename, Predicate excludePackageFilter) throws IOException {
    	checkNotNull(packagename, "packagename must not be null");
    	Predicate packageFilter = buildPackageFilter(packagename,excludePackageFilter);
    	LinkedHashSet> classes = new LinkedHashSet<>();
        long time = System.currentTimeMillis();
        if(Strings.isNullOrEmpty(appPackageName)) {
            try {
                appPackageName = getAppPackageName();
            } catch (Exception e) {
                SimpleLog.log("get app packagename fail",e);
                throw new IOException("get app packagename fail");
            }
        }
//            String sourceDir = context.getApplicationInfo().sourceDir;
        if(Strings.isNullOrEmpty(apkSourceDir)) {
            try {
                apkSourceDir = getApplicationSourceDir(classLoader);
            } catch (Exception e){
                SimpleLog.log("get application source dir fail",e);
                throw new IOException("get application source dir fail");
            }
        }
        SimpleLog.log(debugOutout,"查询应用信息用时:{}",System.currentTimeMillis() - time);
        time = System.currentTimeMillis();

        List classNames = new ArrayList<>();

        DexFile dexfile = new DexFile(apkSourceDir);
        Enumeration entries = dexfile.entries();
        while (entries.hasMoreElements()){
            classNames.add(entries.nextElement());
        }

        classNames.addAll(getSecondaryDexClassNames());

        SimpleLog.log(debugOutout,"查找所有Class名称用时:{}",System.currentTimeMillis() - time);
        time = System.currentTimeMillis();

        FluentIterable.from(classNames)
	        .filter(packageFilter)
	        .transform(CLASS_FORNAME_GET_FUN)
	        .copyInto(classes);
        SimpleLog.log(debugOutout,"加载Class用时:{}",System.currentTimeMillis() - time);
        return classes;
    }

    /**
     * 获取应用的包名
     *  */
    private String getAppPackageName() throws Exception {
        Class clazz = Class.forName("android.app.ActivityThread");
        Method currentPackage = clazz.getMethod("currentPackageName");
        String packageName = (String)currentPackage.invoke(null);
        if(Strings.isNullOrEmpty(packageName)) {
            throw new IOException("packagename is empty");
        }
        SimpleLog.log(debugOutout,"appPackageName = {}",packageName);
        return packageName;
    }

    /**
     * 获取应用apk路径
     * 通过反射机制,获取 BaseClassLoader中的 DexPathList,再得到 Element[] dexElements,最后得到应用apk路径
     * @param classLoader
     * @return 示例:/data/app/com.zykj.androidtest-2/base.apk
     * */
    private String getApplicationSourceDir(ClassLoader classLoader) throws Exception{
        String sourcePath = null;
        try {
            // 反射获取 BaseDexClassLoader 类对象
            Class systemBaseDexClassLoaderClass = Class.forName("dalvik.system.BaseDexClassLoader");
            // 反射获取 BaseDexClassLoader 中的 private final DexPathList pathList 字段
            Field systemPathListField = systemBaseDexClassLoaderClass.getDeclaredField("pathList");
            // 由于是私有成员字段 , 需要设置可访问性
            systemPathListField.setAccessible(true);
            // 获取系统的 PathClassLoader pathClassLoader 对象的
            // private final DexPathList pathList 成员
            Object systemPathListObject = systemPathListField.get(classLoader);
            // 获取 DexPathList 类
            Class systemPathListClass = systemPathListObject.getClass();
            // 获取 DexPathList 类中的 private final Element[] dexElements 成员字段
            Field systemDexElementsField = systemPathListClass.getDeclaredField("dexElements");
            // 由于是私有成员字段 , 需要设置可访问性
            systemDexElementsField.setAccessible(true);
            // 反射获取 DexPathList 对象中的 private Element[] dexElements 成员变量对象
            Object[] oldElements = (Object[])systemDexElementsField.get(systemPathListObject);

            if(oldElements.length>0){
                String info = oldElements[0].toString();
                //示例:zip file "/data/app/com.zykj.androidtest-2/base.apk"
                int index = info.indexOf("\"");
                if(index>0){
                    sourcePath = info.substring(index+1,info.length()-1);
                    SimpleLog.log(debugOutout,"application sourceDir = {}",sourcePath);
                }
            }
        } catch (Exception e) {
            SimpleLog.log(debugOutout,"Failed to get Application Source Dir via reflection",e);
        }
        if(Strings.isNullOrEmpty(sourcePath)) {
            SimpleLog.log(debugOutout,"Use the  command instead");
            sourcePath = getApplicationSourceDirByAdb();
            if(Strings.isNullOrEmpty(sourcePath)) {
                throw new Exception("get application source dir fail");
            }
        }
        return sourcePath;
    }

    /**
     * 获取应用apk路径
     * @return 示例:/data/app/com.zykj.androidtest-2/base.apk
     * */
    private String getApplicationSourceDirByAdb() throws IOException{
        //可以列出所有包信息
        List paths = execAdbCmd("pm list packages -f");
        //结果示例:
        //.....
        //package:/data/app/com.zykj.androidtest.test-1/base.apk=com.zykj.androidtest.test
        //package:/data/app/com.zykj.androidtest-2/base.apk=com.zykj.androidtest

        String pageName2 = "="+appPackageName;
        for(String path:paths){
            if(path.endsWith(pageName2)){
                String sourceDir = path.replace("package:","").replace(pageName2,"");
                SimpleLog.log(debugOutout,"application sourceDir = {}",sourceDir);
                return sourceDir;
            }
        }
        return null;
    }

    /** 执行adb命令 */
    private List execAdbCmd(String cmd) throws IOException{
        List data = new ArrayList<>();
        java.lang.Process pp = Runtime.getRuntime().exec(cmd);
        InputStreamReader ir = new InputStreamReader(pp.getInputStream());
        LineNumberReader input = new LineNumberReader(ir);

        String str = "";
        while(null != str) {
            str = input.readLine();
            if (str != null) {
                data.add(str.trim());
            }
        }
        return data;
    }

    private List getSecondaryDexClassNames() throws IOException {
        List secondryDexFiles = multiDexExtractorLoad(false);

        List classNames = new ArrayList<>();
        for (File file : secondryDexFiles) {
            String path = file.getAbsolutePath();
            try {
                DexFile dexfile;
                if (path.endsWith(".zip")) {
                    // NOT use new DexFile(path), because it will throw "permission error in /data/dalvik-cache"
                    dexfile = DexFile.loadDex(path, path + ".tmp", 0);
                } else {
                    dexfile = new DexFile(path);
                }

                Enumeration entries = dexfile.entries();
                while (entries.hasMoreElements()){
                    classNames.add(entries.nextElement());
                }
            } catch (IOException e) {
                SimpleLog.log("Error at loading dex file '" + path + "'",e);
                throw new IOException("Error at loading dex file '" + path + "'");
            }
        }
        return classNames;
    }

    /**
     * 加载 dex 文件
     * 把 APK(/data/app/{包名}/base.apk)中的 dex文件抽取到 dexDir(/data/user/0/{包名}/code_cache/secondary-dexes)目录中,
     * 然后把 dex 文件路径返回
     * @param forceReload 是否强制重新加载(如果不强制重新加载,则代表如果已经抽取过了,可以直接从缓存目录中拿来使用,这么做速度比较快)
     * @return 返回的files集合有可能为空,表示没有secondaryDex
     * */
    private List multiDexExtractorLoad(boolean forceReload) throws IOException {
        SimpleLog.log(debugOutout,"MultiDexExtractor.load({},{})",apkSourceDir,forceReload);
        //示例输出:MultiDexExtractor.load(/data/app/com.zykj.androidtest-1/base.apk, false)

        sharedPrefsXmlPath = "/data/user/0/" + appPackageName + "/shared_prefs/multidex.version.xml";
        HashMap> params = readXml(sharedPrefsXmlPath);
        int totalDexNumber = getIntFromParams(params,"dex.number", 1);

        dexDir = "/data/user/0/" + appPackageName + "/code_cache/secondary-dexes";
        File dexDirFile = new File(dexDir);
        File sourceApk = new File(apkSourceDir);
        long currentCrc = getZipCrc(sourceApk);
        List files;
        if (!forceReload && !isModified(params,sourceApk, currentCrc)) {
            try {
                files = loadExistingExtractions(sourceApk, dexDirFile,totalDexNumber);
            } catch (IOException var9) {
                SimpleLog.log(debugOutout,"Failed to reload existing extracted secondary dex files, falling back to fresh extraction: {}",var9.getMessage());
                files = performExtractions(sourceApk, dexDirFile);
                putStoredApkInfo(getTimeStamp(sourceApk), currentCrc, files.size() + 1);
            }
        } else {
            SimpleLog.log(debugOutout,"Detected that extraction must be performed.");
            files = performExtractions(sourceApk, dexDirFile);
            putStoredApkInfo(getTimeStamp(sourceApk), currentCrc, files.size() + 1);
        }

        SimpleLog.log(debugOutout,"load found " + files.size() + " secondary dex files");
        return files;
    }

    private List loadExistingExtractions(File sourceApk, File dexDir,int totalDexNumber) throws IOException {
        SimpleLog.log(debugOutout,"loading existing secondary dex files");
        String extractedFilePrefix = sourceApk.getName() + ".classes";
        List files = new ArrayList<>(totalDexNumber);

        for(int secondaryNumber = 2; secondaryNumber <= totalDexNumber; ++secondaryNumber) {
            String fileName = extractedFilePrefix + secondaryNumber + ".zip";
            File extractedFile = new File(dexDir, fileName);
            if (!extractedFile.isFile()) {
                throw new IOException("Missing extracted secondary dex file '" + extractedFile.getPath() + "'");
            }

            files.add(extractedFile);
            if (!verifyZipFile(extractedFile)) {
                SimpleLog.log(debugOutout,"Invalid zip file: " + extractedFile);
                throw new IOException("Invalid ZIP file.");
            }
        }
        return files;
    }

    private boolean isModified(HashMap> params,File archive, long currentCrc) {
        long timestamp = getLongFromParams(params,"timestamp", -1L);
        long crc = getLongFromParams(params,"crc", -1L);
        return timestamp != getTimeStamp(archive) || crc != currentCrc;
    }

    private long getTimeStamp(File archive) {
        long timeStamp = archive.lastModified();
        if (timeStamp == -1L) {
            --timeStamp;
        }

        return timeStamp;
    }

    private long getZipCrc(File archive) throws IOException {
        long computedValue = ZipUtil.getZipCrc(archive);
        if (computedValue == -1L) {
            --computedValue;
        }

        return computedValue;
    }

    private List performExtractions(File sourceApk, File dexDir) throws IOException {
        String extractedFilePrefix = sourceApk.getName() + ".classes";
        prepareDexDir(dexDir, extractedFilePrefix);
        List files = new ArrayList<>();
        ZipFile apk = new ZipFile(sourceApk);

        try {
            int secondaryNumber = 2;

            for(ZipEntry dexFile = apk.getEntry("classes" + secondaryNumber + ".dex"); dexFile != null; dexFile = apk.getEntry("classes" + secondaryNumber + ".dex")) {
                String fileName = extractedFilePrefix + secondaryNumber + ".zip";
                File extractedFile = new File(dexDir, fileName);
                files.add(extractedFile);
                SimpleLog.log(debugOutout,"Extraction is needed for file {}",extractedFile);
                // 示例输出:Extraction is needed for file /data/user/0/com.zykj.androidtest/code_cache/secondary-dexes/base.apk.classes2.zip
                int numAttempts = 0;
                boolean isExtractionSuccessful = false;

                while(numAttempts < 3 && !isExtractionSuccessful) {
                    ++numAttempts;
                    extract(apk, dexFile, extractedFile, extractedFilePrefix);
                    isExtractionSuccessful = verifyZipFile(extractedFile);
                    SimpleLog.log(debugOutout,"Extraction {} - length {}: ",isExtractionSuccessful ? "success" : "failed", extractedFile.getAbsolutePath(),extractedFile.length());
                    //示例输出:Extraction success - length /data/user/0/com.zykj.androidtest/code_cache/secondary-dexes/base.apk.classes2.zip: 2311419
                    if (!isExtractionSuccessful) {
                        extractedFile.delete();
                        if (extractedFile.exists()) {
                            SimpleLog.log(debugOutout,"Failed to delete corrupted secondary dex '{}'",extractedFile.getPath());
                        }
                    }
                }

                if (!isExtractionSuccessful) {
                    throw new IOException("Could not create zip file " + extractedFile.getAbsolutePath() + " for secondary dex (" + secondaryNumber + ")");
                }

                ++secondaryNumber;
            }
        } finally {
            try {
                apk.close();
            } catch (IOException var16) {
                SimpleLog.log(debugOutout,"Failed to close resource. error:{}",var16.getMessage());
            }

        }

        return files;
    }

    private void putStoredApkInfo(long timeStamp, long crc, int totalDexNumber) throws IOException{
//        SharedPreferences prefs = getMultiDexPreferences(context);
//        SharedPreferences.Editor edit = prefs.edit();
//        edit.putLong("timestamp", timeStamp);
//        edit.putLong("crc", crc);
//        edit.putInt("dex.number", totalDexNumber);

        HashMap> data = new HashMap<>();
        HashMap item = new HashMap<>();
        item.put("type","long");
        item.put("value",""+timeStamp);
        data.put("timestamp",item);
        item = new HashMap<>();
        item.put("type","long");
        item.put("value",""+crc);
        data.put("crc",item);
        item = new HashMap<>();
        item.put("type","int");
        item.put("value",""+totalDexNumber);
        data.put("dex.number",item);

        editXml(sharedPrefsXmlPath,data);
    }

    private void prepareDexDir(File dexDir, final String extractedFilePrefix) throws IOException {
        File cache = dexDir.getParentFile();
        mkdirChecked(cache);
        mkdirChecked(dexDir);
        FileFilter filter = new FileFilter() {
            public boolean accept(File pathname) {
                return !pathname.getName().startsWith(extractedFilePrefix);
            }
        };
        File[] files = dexDir.listFiles(filter);
        if (files == null) {
            SimpleLog.log(debugOutout,"Failed to list secondary dex dir content (" + dexDir.getPath() + ").");
        } else {
            File[] arr$ = files;
            int len$ = files.length;

            for(int i$ = 0; i$ < len$; ++i$) {
                File oldFile = arr$[i$];
                SimpleLog.log(debugOutout,"Trying to delete old file " + oldFile.getPath() + " of size " + oldFile.length());
                if (!oldFile.delete()) {
                    SimpleLog.log(debugOutout,"Failed to delete old file " + oldFile.getPath());
                } else {
                    SimpleLog.log(debugOutout,"Deleted old file " + oldFile.getPath());
                }
            }

        }
    }

    private void mkdirChecked(File dir) throws IOException {
        dir.mkdir();
        if (!dir.isDirectory()) {
            File parent = dir.getParentFile();
            if (parent == null) {
                SimpleLog.log(debugOutout,"Failed to create dir " + dir.getPath() + ". Parent file is null.");
            } else {
                SimpleLog.log(debugOutout,"Failed to create dir " + dir.getPath() + ". parent file is a dir " + parent.isDirectory() + ", a file " + parent.isFile() + ", exists " + parent.exists() + ", readable " + parent.canRead() + ", writable " + parent.canWrite());
            }

            throw new IOException("Failed to create cache directory " + dir.getPath());
        }
    }

    private void extract(ZipFile apk, ZipEntry dexFile, File extractTo, String extractedFilePrefix) throws IOException, FileNotFoundException {
        InputStream in = apk.getInputStream(dexFile);
        ZipOutputStream out = null;
        File tmp = File.createTempFile(extractedFilePrefix, ".zip", extractTo.getParentFile());
        SimpleLog.log(debugOutout,"Extracting {}",tmp.getPath());
        //示例输出:Extracting /data/user/0/com.zykj.androidtest/code_cache/secondary-dexes/base.apk.classes1997043754.zip

        try {
            out = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(tmp)));

            try {
                ZipEntry classesDex = new ZipEntry("classes.dex");
                classesDex.setTime(dexFile.getTime());
                out.putNextEntry(classesDex);
                byte[] buffer = new byte[16384];

                for(int length = in.read(buffer); length != -1; length = in.read(buffer)) {
                    out.write(buffer, 0, length);
                }

                out.closeEntry();
            } finally {
                out.close();
            }

            SimpleLog.log(debugOutout,"Renaming to {}",extractTo.getPath());
            //示例输出:Renaming to /data/user/0/com.zykj.androidtest/code_cache/secondary-dexes/base.apk.classes2.zip
            if (!tmp.renameTo(extractTo)) {
                throw new IOException("Failed to rename \"" + tmp.getAbsolutePath() + "\" to \"" + extractTo.getAbsolutePath() + "\"");
            }
        } finally {
            closeQuietly(in);
            tmp.delete();
        }

    }

    private boolean verifyZipFile(File file) {
        try {
            ZipFile zipFile = new ZipFile(file);

            try {
                zipFile.close();
                return true;
            } catch (IOException var3) {
                SimpleLog.log(debugOutout,"Failed to close zip file: {}",file.getAbsolutePath());
            }
        } catch (ZipException var4) {
            SimpleLog.log(debugOutout,"File " + file.getAbsolutePath() + " is not a valid zip file.   error:"+var4.getMessage());
        } catch (IOException var5) {
            SimpleLog.log(debugOutout,"Got an IOException trying to open zip file: " + file.getAbsolutePath()+"  error:"+ var5.getMessage());
        }

        return false;
    }

    private void closeQuietly(Closeable closeable) {
        try {
            closeable.close();
        } catch (IOException var2) {
            SimpleLog.log(debugOutout,"Failed to close resource. error:{}",var2.getMessage());
        }

    }

    /**
     * 存储信息到xml文档,如果文件不存在,则创建
     * @param filePath 文件地址
     * @param data 需要添加的内容
     * */
    private void editXml(String filePath, HashMap> data) throws IOException{
        long time = System.currentTimeMillis();
        try {
            Document document = null;
            File docFile = new File(filePath);
            if(docFile.exists()) {
                //解析文档
                DocumentBuilderFactory dbf=DocumentBuilderFactory.newInstance();
                DocumentBuilder db=dbf.newDocumentBuilder();
                document=db.parse(docFile);
            } else {
                //创建文档
                if(!docFile.getParentFile().exists()){
                    docFile.getParentFile().mkdirs();
                }
                docFile.createNewFile();
                DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
                DocumentBuilder builder = factory.newDocumentBuilder();
                document = builder.newDocument();
                document.setXmlStandalone(true);
            }

            Element mapNode = null;
            boolean mapNodeIsNew = false;
            NodeList mapNodeList=document.getElementsByTagName("map");
            if(mapNodeList!=null && mapNodeList.getLength()>0){
                mapNode = (Element)mapNodeList.item(0);
                mapNodeIsNew = true;
            }
            if(mapNode==null){
                mapNode = document.createElement("map");
            }
            for (Map.Entry> entry : data.entrySet()) {
                String name = entry.getKey();
                String type = entry.getValue().get("type");
                String value = entry.getValue().get("value");

                boolean isNodeExists= false;
                NodeList typeNodeList=document.getElementsByTagName(type);
                for(int i=0;i0){
                        Node nameN = nMap.getNamedItem("name");
                        Node valueN = nMap.getNamedItem("value");
                        if(Objects.equals(nameN.getNodeValue(),name)){
                            isNodeExists = true;
                            if(!Objects.equals(valueN.getNodeValue(),value)){
                                //修改值
                                valueN.setNodeValue(value);
                            }
                            break;
                        }
                    }
                }
                if(!isNodeExists) {
                    //新增值
                    Element item = document.createElement(type);
                    item.setAttribute("name",name);
                    item.setAttribute("value",value);
                    mapNode.appendChild(item);
                }
            }
            if(!mapNodeIsNew){
                document.appendChild(mapNode);
            }

            TransformerFactory transformerFactory = TransformerFactory.newInstance();
            Transformer transformer = transformerFactory.newTransformer();
            //这个是显示格式化用的
            transformer.setOutputProperty(OutputKeys.INDENT, "yes");
            transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2");
            DOMSource source = new DOMSource(document);
            StreamResult result = new StreamResult(docFile);
            transformer.transform(source,result);
//        tf.transform(new DOMSource(document), new StreamResult(System.out)); //输出控制台
        } catch (Exception e){
            SimpleLog.log(String.format("Save File [%s] ERROR",filePath),e);
            throw new IOException(String.format("Save File [%s] ERROR",filePath));
        }
        SimpleLog.log(debugOutout,"存储XML文件用时:{}",System.currentTimeMillis() - time);
    }

    /**
     * 读取XML文档里的 map节点的所有内容
     * @param filePath 文件地址
     * @return
     * */
    private HashMap> readXml(String filePath) throws IOException{
        long time = System.currentTimeMillis();
        HashMap> data = new HashMap<>();
        File file=new File(filePath);
        if(!file.exists()){
            return data;
        }
        DocumentBuilderFactory dbf=DocumentBuilderFactory.newInstance();
        DocumentBuilder db;
        try {
            db=dbf.newDocumentBuilder();
            Document document=db.parse(file);
            NodeList mapNodeList=document.getElementsByTagName("map");
            if(mapNodeList!=null && mapNodeList.getLength()>0){
                NodeList mapChildList=mapNodeList.item(0).getChildNodes();
                for(int i=0;i0){
                        Node nameN = nMap.getNamedItem("name");
                        String name = nameN.getNodeValue();
                        Node valueN = nMap.getNamedItem("value");
                        String value = valueN.getNodeValue();
                        if(!Strings.isNullOrEmpty(name) && !Strings.isNullOrEmpty(value)) {
                            HashMap map = new HashMap<>();
                            map.put("type",type);
                            map.put("value",value);
                            data.put(name,map);
                        }
                    }
                }
            }
        } catch (Exception e) {
            SimpleLog.log(String.format("Read File [%s] ERROR",filePath),e);
            throw new IOException(String.format("Read File [%s] ERROR",filePath));
        }
        SimpleLog.log(debugOutout,"读取XML文件用时:{}",System.currentTimeMillis() - time);
        return data;
    }

    private int getIntFromParams(HashMap> params,String key,int defaultValue){
        if(params!=null){
            HashMap map = params.get(key);
            if(map!=null){
                String valueStr = map.get("value");
                try {
                    Integer valueInt = Integer.parseInt(valueStr);
                    return valueInt.intValue();
                } catch (NumberFormatException e) {
                    e.printStackTrace();
                }
            }
        }
        return defaultValue;
    }

    private long getLongFromParams(HashMap> params,String key,long defaultValue){
        if(params!=null){
            HashMap map = params.get(key);
            if(map!=null){
                String valueStr = map.get("value");
                try {
                    Long valueL = Long.parseLong(valueStr);
                    return valueL.longValue();
                } catch (NumberFormatException e) {
                    e.printStackTrace();
                }
            }
        }
        return defaultValue;
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy