Search code examples
javaandroidxmlaaptaapt2

Converting standard xml file to formated binary AXMl file


I'm trying to convert a standard XML file

Like

<vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:width="30dp"
    android:height="30dp"
    android:viewportWidth="24"
    android:viewportHeight="24">
  <path
      android:fillColor="#5CDD06"
      android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM10,17l-5,-5 1.41,-1.41L10,14.17l7.59,-7.59L19,8l-9,9z"/>
</vector>

to binary formated xml file so that I can inflate Views and drawables at runtime

But I can't find the right way. I tried to get the bytes from the file

From


FileInputStream is = new FileInputStream("file.xml");

byte[] arr = new byte[is.available];

is.read(arr)
 

and try to parse it to View or Drawlable but Xml$Block.Parser can't handle it

    @SuppressLint("PrivateApi")
Class<?> xmlBlock = Class.forName("android.content.res.XmlBlock");

Constructor xmlBlockConstr = xmlBlock.getConstructor(byte[].class);

Method xmlParserNew = xmlBlock.getDeclaredMethod("newParser");

xmlBlockConstr.setAccessible(true);

xmlParserNew.setAccessible(true);

XmlPullParser parser = (XmlPullParser) xmlParserNew.invoke(xmlBlockConstr.newInstance((Object) arr2)); //throws invocationTargetException
Drawable.createFromXml(context.getResources, parser);

it throws InvocationTargetException

But when i use this method

public static byte[] createBinaryDrawableXml(int width, int height,
                                                  float viewportWidth, float viewportHeight,
                                                  List<PathData> paths) {
        List<byte[]> stringPool = new ArrayList<>(Arrays.asList(BIN_XML_STRINGS));
        for (PathData path : paths) {
            stringPool.add(path.data);
        }

        ByteBuffer bb = ByteBuffer.allocate(8192);  // Capacity might have to be greater.
        bb.order(ByteOrder.LITTLE_ENDIAN);

        int posBefore;

        // ==== XML chunk ====
        // https://justanapplication.wordpress.com/2011/09/22/android-internals-binary-xml-part-two-the-xml-chunk/
        bb.putShort(CHUNK_TYPE_XML);  // Type
        bb.putShort((short) 8);  // Header size
        int xmlSizePos = bb.position();
        bb.position(bb.position() + 4);

        // ==== String pool chunk ====
        // https://justanapplication.wordpress.com/2011/09/15/android-internals-resources-part-four-the-stringpool-chunk/
        int spStartPos = bb.position();
        bb.putShort(CHUNK_TYPE_STR_POOL);  // Type
        bb.putShort((short) 28);  // Header size
        int spSizePos = bb.position();
        bb.position(bb.position() + 4);
        bb.putInt(stringPool.size());  // String count
        bb.putInt(0);  // Style count
        bb.putInt(1 << 8);  // Flags set: encoding is UTF-8
        int spStringsStartPos = bb.position();
        bb.position(bb.position() + 4);
        bb.putInt(0);  // Styles start

        // String offsets
        int offset = 0;
        for (byte[] str : stringPool) {
            bb.putInt(offset);
            offset += str.length + (str.length > 127 ? 5 : 3);
        }

        posBefore = bb.position();
        bb.putInt(spStringsStartPos, bb.position() - spStartPos);
        bb.position(posBefore);

        // String pool
        for (byte[] str : stringPool) {
            if (str.length > 127) {
                byte high = (byte) ((str.length & 0xFF00 | 0x8000) >>> 8);
                byte low = (byte) (str.length & 0xFF);
                bb.put(high);
                bb.put(low);
                bb.put(high);
                bb.put(low);
            } else {
                byte len = (byte) str.length;
                bb.put(len);
                bb.put(len);
            }
            bb.put(str);
            bb.put((byte) 0);
        }

        if (bb.position() % 4 != 0) {
            // Padding to align on 32-bit
            bb.put(new byte[4 - (bb.position() % 4)]);
        }

        // Write string pool chunk size
        posBefore = bb.position();
        bb.putInt(spSizePos, bb.position() - spStartPos);
        bb.position(posBefore);

        // ==== Resource map chunk ====
        // https://justanapplication.wordpress.com/2011/09/23/android-internals-binary-xml-part-four-the-xml-resource-map-chunk/
        bb.putShort(CHUNK_TYPE_RES_MAP);  // Type
        bb.putShort((short) 8);  // Header size
        bb.putInt(8 + BIN_XML_ATTRS.length * 4);  // Chunk size
        for (int attr : BIN_XML_ATTRS) {
            bb.putInt(attr);
        }

        // ==== Vector start tag ====
        int vstStartPos = bb.position();
        int vstSizePos = putStartTag(bb, 7, 4);

        // Attributes
        // android:width="24dp", value type: dimension (dp)
        putAttribute(bb, 0, -1, VALUE_TYPE_DIMENSION, (width << 8) + 1);

        // android:height="24dp", value type: dimension (dp)
        putAttribute(bb, 1, -1, VALUE_TYPE_DIMENSION, (height << 8) + 1);

        // android:viewportWidth="24", value type: float
        putAttribute(bb, 2, -1, VALUE_TYPE_FLOAT, Float.floatToRawIntBits(viewportWidth));

        // android:viewportHeight="24", value type: float
        putAttribute(bb, 3, -1, VALUE_TYPE_FLOAT, Float.floatToRawIntBits(viewportHeight));

        // Write vector start tag chunk size
        posBefore = bb.position();
        bb.putInt(vstSizePos, bb.position() - vstStartPos);
        bb.position(posBefore);

        for (int i = 0; i < paths.size(); i++) {
            // ==== Path start tag ====
            int pstStartPos = bb.position();
            int pstSizePos = putStartTag(bb, 6, 2);

            // android:fillColor="#aarrggbb", value type: #rgb.
            putAttribute(bb, 4, -1, VALUE_TYPE_COLOR, paths.get(i).color);

            // android:pathData="...", value type: string
            putAttribute(bb, 5, 9 + i, VALUE_TYPE_STRING, 9 + i);

            // Write path start tag chunk size
            posBefore = bb.position();
            bb.putInt(pstSizePos, bb.position() - pstStartPos);
            bb.position(posBefore);

            // ==== Path end tag ====
            putEndTag(bb, 6);
        }

        // ==== Vector end tag ====
        putEndTag(bb, 7);

        // Write XML chunk size
        posBefore = bb.position();
        bb.putInt(xmlSizePos, bb.position());
        bb.position(posBefore);

        // Return binary XML byte array
        byte[] binXml = new byte[bb.position()];
        bb.rewind();
        bb.get(binXml);

        return binXml;
    }

And try to invoke the newParser method

List<PathData> pathList = Arrays.asList(new PathData("M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM10,17l-5,-5 1.41,-1.41L10,14.17l7.59,-7.59L19,8l-9,9z", Color.parseColor("#5CDD06")));


byte[] arr2 = createBinaryDrawableXml(30, 30, 24, 24, pathList);

XmlPullParser parser = (XmlPullParser) xmlParserNew.invoke(xmlBlockConstr.newInstance((Object) arr2));

Drawable.createFromXml(context.getResources, parser);

It works like charm and the Drawable shows

And i tried aapt but its packging the hole resources and put into apk

I found this question but no answer

How to use Android aapt to compile a specific layout file to binary?

I tried to use https://github.com/hzw1199/xml2axml

But im getting

xmlpullparser exception at line 7 height must be > 0

So any solutions?


Solution

  • Solved

    After a lot of work and tracing android java source code i found very acceptable solution

    The real question was how to inflate views at run time

    We all know that you cant

    LayoutInflater

    Important For performance reasons, view inflation relies heavily on pre-processing of XML files that is done at build time. Therefore, it is not currently possible to use LayoutInflater with an XmlPullParser over a plain XML file at runtime.

    Because android compiling resources at compile time then convert it to bytes then use XmlBlock Class

    That class only accept Android xml format (Axml)

    And LayoutInflater class casting XmlBlock to XmlPullParser

    So when you try to inflate views with

    LayoutInflater.inflate(XmlPullParser,root);
    

    You got cast Exception

    Anyway here is the Solution

    First i tried to convert xml to axml using Aapt tool and getting XmlBlock private class by reflection and inflate the xml file

    That works but!

    If i add attribute like

    android:background="@drawable/resource"

    He couldn't find the resource cuz i only compiled the layout xml file and inflate it

    I tried a lot solutions and wasted a lot of time but nothing work

    Then!

    While tracing android source code found how are resource made programmatically

    First you have to get ContextImpl private Class

    Class contextImpl = Class.forName("android.app.ContextImpl");
    

    Then create new Context or in other words clone it!

    Dont worry we can clone the Context just keep up with me :>

    To create new context we have to call createApplicationContext

    This method takes ApplicationInfo argument and flags

    Method createContext = contextImpl.getMethod("createApplicationContext", new Class<?>[]{ApplicationInfo.class, int.class});
    

    And getting newInstance of ContextImpl

    Method getImpl = contextImpl.getDeclaredMethod("getImpl",Context.class);
    getImpl.setAccessible(true);
    Object cImpl = getImpl.invoke(null,context);//orignal app context
    

    Then excute method to get the cloned Context

    But we have to send the orignal app context to get clone instance of it

    Context newContext = (Context)createContext.invoke(cImpl,new Object[]{context.getApplicationInfo(),0});// my application info
    

    Done we have cloned context

    Now we nees to change the Resources in cloned context to our resources

    So you have to create your full /res directory and put your resources in

    Then we use Aapt to get compiled resources

    Here is download link Aapt

    Let's back to work!

    Now compile our resources

    String[] cmd2 = {context.getFilesDir().getAbsolutePath()+ "/aapt","p","-m","-f","-v","-M",
                    "/storage/emulated/0/AppProjects/XmlToView/app/src/main/AndroidManifest.xml","-I"
                    ,"/storage/emulated/0/Download/android.jar",
                    "-S","/storage/emulated/0/UI Projects/Test/res","-S","/storage/emulated/0/appcompat-v7-28.0.0/res","-F",
                    "/storage/emulated/0/UI Projects/Test/compiledRes.apk"
                    ,"-J",path.substring(0,path.length() - "res".length()),
                    "--extra-packages","android.support.v7.appcompat","--auto-add-overlay"};
                
    Runtime.getRuntime().exec(cmd2).waitFor();
    

    You can see documentation too learn how aapt works

    Now we have occurred apk file but it doesn't matter we just need the resources

    So now we can make our custom resources instance

    First we have to get private Class ResourcesManager To create new rssources class

    Class resourceManager = Class.forName("android.app.ResourcesManager");
    

    And get new instance using getInstace method

        
    Method getInstance = resourceManager.getDeclaredMethod("getInstance");
    Object reso = getInstance.invoke(null);
    

    Now we are ready to create new Resouces

    Now we have to find getResources method

    I found it using loop because it takes 11 argument

    Now you have to get compiled res apk file path that we created using aapt

    And path it in get resources method

    Method[] methods = resourceManager.getDeclaredMethods();
                Resources newResss = null;
                for (Method mthd: methods)
                {
                    String mthdName = mthd.getName();
    
                    if (mthdName.equals("getResources") && mthd.getParameterCount() == 11)
                    {
                        newResss =(Resources) mthd.invoke(reso , new Object[]{
                                                              null,
                                                              "Compiled res path /storage/resources.apk",
                                                              null,
                                                              null,
                                                              null,
                                                              0,
                                                              null,
                                                              null,
                                                              null,
                                                              null,
                                                              0
                                                          });
    
                        break;
                    }
                }
    

    Now we have a new Resources thet contains all your resources you make

    All what left is to change the cloned context resources to the new Resources

    Getting setResources method

    if(newResss != null){
        Method setResources = newContext.getClass().getDeclaredMethod("setResources",Resources.class);
        setResources.setAccessible(true);
     
      
     setResources.invoke(newContext,newResss);//passing new Context and new Resources
                }
    

    Now you have new context of your app with deffrent Resources

    Now all you have to do to inflate the views is creating new LayoutInflater with the new context and calling inflate

    But you have to get id of layout you need to inflate and of course you will not found it in R class

    So take this method

    public int getJavaFileResource(File f, String name){
            String[] result = new String[3];
            try
            {
                Reader r = new FileReader(f);
                BufferedReader br = new BufferedReader(r);
    
                String s;
                while ((s = br.readLine()) != null)
                {
                    if (s.contains("class") && s.contains("public"))
                    {
                        int index = s.indexOf("class") + "class".length() + 1;
                        String cls = s.substring(index, s.indexOf(" ", index));
                        result[0] = cls;
    
                    }
                    else if (s.contains("public") && s.contains("int"))
                    {
                        int index = s.indexOf("int") + "int".length() + 1;
                        String name2 = s.substring(index, s.indexOf("="));
                        String value = s.substring(s.indexOf("=") + 1, s.length() - 1);
    
                        int vvalue = 0;
                        try{
                            vvalue = Integer.decode(value);
                        }
                        catch(NumberFormatException e){
                            continue;
                        }
                        if (name.equals(name2))
                        {
    
                            return vvalue;
                        }
                    }
                }
            }
            catch (IOException e)
            {}
    
    
            return 0;
        }
    

    Aapt tool will generate new R java file in your choosed path

    You have to read the documentation of aapt

    So you excute this method getJavaResource and path R.java file and resource name like main ..

    Now all you have to do calling that

    LayoutInflater.from(newContext).inflate(resid,root);
    

    Congratulations now you have inflated layout at runtime.

    Hope this works for all devices

    Bye.