1

Background

My app finds APK files in the storage of the device, showing information about them and allowing to perform operations on them.

The problem

Thing is, ever since Google added new restrictions about storage, I'm trying to find solutions about how to parse APK files without storage permission (because the framework can handle only file-path). For now I use the special legacy flag (requestLegacyExternalStorage) to allow me to do it with file-path, but it's a temporary solution till the next version of Android.

What I've tried and found

Currently, using a workaround and a the library (link here), I succeeded getting the basic information and the app name, but for icons it becomes messy : wrong qualifiers, can return PNG instead of VectorDrawable, and the VectorDrawable is just a byte array...

But even if I ignore its issues, I tried to look at the various VectorDrawable functions and also its android-x ones (which actually looked very promising), to try to create an instance out of what I got. For some reason, each function I tried didn't help.

The question

Is it possible to load VectorDrawable dynamically from byte code? For now I'm planning to use this library which parses APKs on its own (because sadly Google is planning to add restrictions of reaching files, to use the terrible SAF), but sadly for VectorDrawable it just has a byte array...

Just to be clear: I'm talking about accessing the APK files (not installed ones, as getting this information still works fine) from SAF. With storage permission it worked fine (and of course with installed apps). See here. When you try to use SAF with the normal framework, you will get bad results: a string with a package name as the app-name, and the default icon as the app-icon (or worse: an exception). Also note: I know I can copy the files to my own app's storage and use just the framework, but this isn't a good solution, because if I want to show information about all APK files on the device, it would be a waste of space and time, just to get the icons and app-names... I need a more direct approach. One that is fastest for the app to handle.

android developer
  • 114,585
  • 152
  • 739
  • 1,270
  • Do you know the format of the byte array? Is it binary XML? If so you're in luck, you just need to parse it and feed it to VectorDrawableCompat, like I did in the `getVectorDrawable` method. Someone made a parser [here](https://github.com/ytsutano/axmldec), I guess you could make JNI bindings and use it. – Nicolas Nov 24 '19 at 22:10
  • @Nicolas Pretty sure it's in binary form. Can you please share a POC of doing it this way? Of parsing from binary byte array, and getting VectorDrawable from it? Why did you write about a parser though, if you use something that's already there? – android developer Nov 24 '19 at 23:06
  • Actually I don't know why I talked about parsing, you shouldn't need that at all. In fact my other answer *created* binary XML in order to include the path data in it. But if you already have binary XML, you just have to create the drawable with it. Since there's no constructor for that this is what the method in my answer does by using reflection. – Nicolas Nov 24 '19 at 23:58

1 Answers1

2

If the byte array of the vector drawable you have has a binary XML form, then you should be able to create it by passing the array to the following method I took from my previous answer:

/**
 * Create a vector drawable from a binary XML byte array.
 * @param context Any context.
 * @param binXml Byte array containing the binary XML.
 * @return The vector drawable or null it couldn't be created.
 */
public static Drawable getVectorDrawable(@NonNull Context context, @NonNull byte[] binXml) {
    try {
        // Get the binary XML parser (XmlBlock.Parser) and use it to create the drawable
        // This is the equivalent of what AssetManager#getXml() does
        @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) binXml));

        if (Build.VERSION.SDK_INT >= 24) {
            return Drawable.createFromXml(context.getResources(), parser);
        } else {
            // Before API 24, vector drawables aren't rendered correctly without compat lib
            final AttributeSet attrs = Xml.asAttributeSet(parser);
            int type = parser.next();
            while (type != XmlPullParser.START_TAG) {
                type = parser.next();
            }
            return VectorDrawableCompat.createFromXmlInner(context.getResources(), parser, attrs, null);
        }

    } catch (Exception e) {
        Log.e(TAG, "Vector creation failed", e);
    }
    return null;
}

I haven't tested it but I don't see a reason why it shouldn't work if your byte array really is the binary XML of a vector drawable, which is what the XmlPullParser and VectorDrawable expects to parse.

EDIT: I tried using XmlPullParser to create the drawable like this:

val xml = """
|<vector xmlns:android="http://schemas.android.com/apk/res/android"
|    android:width="24dp"
|    android:height="24dp"
|    android:viewportWidth="24.0"
|    android:viewportHeight="24.0">
|    <path
|        android:pathData="M9.5,3A6.5,6.5 0,0 0,3 9.5A6.5,6.5 0,0 0,9.5 16A6.5,6.5 0,0 0,13.33 14.744L18.586,20L20,18.586L14.742,13.328A6.5,6.5 0,0 0,16 9.5A6.5,6.5 0,0 0,9.5 3zM9.5,5A4.5,4.5 0,0 1,14 9.5A4.5,4.5 0,0 1,9.5 14A4.5,4.5 0,0 1,5 9.5A4.5,4.5 0,0 1,9.5 5z"
|        android:fillColor="#000000"/>
|</vector>
""".trimMargin()

val parser = XmlPullParserFactory.newInstance().newPullParser()
parser.setInput(xml.reader())

val drawable = Drawable.createFromXml(resources, parser, theme)
iconView.setImageDrawable(drawable)

However this fails with an exception: java.lang.ClassCastException: android.util.XmlPullAttributes cannot be cast to android.content.res.XmlBlock$Parser. So this is not possible after all to answer your comment.

Nicolas
  • 6,611
  • 3
  • 29
  • 73
  • Seems promising! Can't wait to try it out! But, why reflection? Isn't there a better workaround for this? I've seen multiple functions of the support library VectorDrawable to create one. None of them really work? I was hoping that I just don't use them correctly... – android developer Nov 25 '19 at 08:55
  • Wow I've tested it and it worked! Great job! How did you find this solution? Is there one without reflection? What if I assume I'm working only on API 24 and above? Would I still need this? Or there is maybe a better choice? Maybe it's possible to create XmlPullParser from byte array without the android-x library at all? – android developer Nov 26 '19 at 21:59
  • I found it by digging for several hours in Android source code. There's no solution without reflection as far as I have seen, since most resource handling classes are part of a private API. No you don't need the androidx library on API 24 and above (although I'm sure you plan on supporting older versions). Also you're saying you're getting a binary XML byte array from the APK, but [adaptive icons](https://developer.android.com/guide/practices/ui_guidelines/icon_design_adaptive) also have a XML definition without being VectorDrawables so you might run into some issues... – Nicolas Nov 26 '19 at 23:06
  • So how would you solve it if you can assume you are running on API 24? Would it be without reflection? Could you use the framework alone, without the android-x library? About the issue, you are correct: How do you suggest to differentiate between VectorDrawable and other types of files? I tried to use this library: https://github.com/xgouchet/AXML , so that I could look at the binary XML file more easily, but it failed to parse the byte array for some reason... – android developer Nov 26 '19 at 23:56
  • On API 24 just remove the clause for API < 24 but reflection is still needed to get the XmlPullParser to parse the byte array. To differentiate between the two types you probably have no choice but to look at the binary XML like you said. I saw your issue on the library you linked, I can't say what's wrong... the library seems dead though so you might have to find a solution by yourself. And binary XML is a PITA to be honest since it's not documented a lot so best of luck with that! – Nicolas Nov 27 '19 at 00:07
  • So still need reflection... Can't I copy just what is needed from what the reflection points to? Or is it just too much or too dependent ? As for XML parsing, I think the library I've pointed to might not be able to handle VectorDrawable, but maybe other types such as manifest. Anyway, I've found this promising repository (by Google?!) : https://github.com/google/android-classyshark . It works with various XML files within the APK, and I can run it on Windows OS. I wonder if it's possible to use it for Android too. – android developer Nov 27 '19 at 00:42
  • 1
    These are internal APIs with lots of dependencies so copying would involve copying a lot (including lots of native code). Reflection isn't always as bad as it looks and in this case, it's clearly the only choice. – Nicolas Nov 27 '19 at 00:52
  • Thought so. I just hope that this solution will last. Have you checked the link I've written? Could it help a bit? What if instead of a byte array for the VectorDrawable, I could get it in a normal XML style way? Would that help to get VectorDrawable ? – android developer Nov 27 '19 at 08:51
  • Yes I checked the library it should work fine I guess. But I thought about it and if you only plan to support API >= 24, then `Vector.createFromXml` should work for all types of drawables! To answer your question, and if I recall, no android reource can be created from non-binary XML – Nicolas Nov 27 '19 at 11:48
  • But you said that if API>=24 I still need reflection, even for Vector.createFromXml , no? – android developer Nov 27 '19 at 17:23
  • Yes reflection is used to create the XmlPullParser, not the drawable itself... I encourage you to explore the platform source code. It takes some time but it's worth it. – Nicolas Nov 27 '19 at 17:24
  • Can't I create an implementation of XmlPullParser after I get the XML of the VectorDrawable? – android developer Nov 27 '19 at 18:53
  • Yes you could but I don't understand why you would convert binary XML to plain XML just to avoid reflection... – Nicolas Nov 27 '19 at 22:14
  • As a fallback, in case the reflection didn't work. Anyway, I tried the library, and it seems worse in terms of restoring the files to their original form. It could get some files fine, but not what I wanted... – android developer Nov 28 '19 at 19:26
  • Can you please show, given a string of the XML (not binary), how to get a VectorDrawable out of it? – android developer Nov 29 '19 at 09:21
  • 1
    See my edit. I was wrong, it doesn't seem to be possible, `Drawable.createFromXml` only takes binary XML input. – Nicolas Dec 01 '19 at 13:47
  • Too bad. I think this is the same exception I got when I tried it. – android developer Dec 01 '19 at 20:33
  • Suppose your input is a binary XML, is it possible to implement XmlPullParser to handle it, and provide it here, instead of reflection? Maybe something the class "BinaryXmlParser" here: https://github.com/hsiafan/apk-parser could help ? I tried using other methods, but got exception `java.lang.ClassCastException: android.util.XmlPullAttributes cannot be cast to android.content.res.XmlBlock$Parser` , so I think it really wants only the XmlPullParser that's made by `XmlBlock` class – android developer May 31 '20 at 11:12
  • The cast fails as you saw. I actually opened an issue [here](https://issuetracker.google.com/issues/146166781) a few months ago to ask if `XmlBlock.newParser()` could become part of the public API. However chances are very slim, and you'll still need reflection for older APIs. What's so bad with reflection anyway? – Nicolas May 31 '20 at 11:32
  • I'm afraid of publishing an app that uses it, seeing that Google is so against it, and because one day it might fail. About the casting, this means it really expects this kind of XmlPullParser , right? The issue you've shared is not reachable. Maybe create a new one? – android developer May 31 '20 at 13:21
  • The issue must be private I guess. For the cast, check [this line in the source](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/content/res/Resources.java;l=1919). It expects only this kind of parser. – Nicolas May 31 '20 at 13:26
  • I see. So there is no way to overcome this for now other than reflection. Here, made a request for XmlBlock: https://issuetracker.google.com/issues/157816266 – android developer May 31 '20 at 13:34
  • 1
    I think we should avoid parsing using the framework, and use only the support library because its results are more stable&reliable . Sadly, this doesn't prevent from reaching the problematic line you've shown. It still expects XmlBlock.Parser . – android developer May 31 '20 at 23:35