Home > database >  Converting standard xml file to formated binary AXMl file
Converting standard xml file to formated binary AXMl file

Time:12-10

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?

CodePudding user response:

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.

  • Related