Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
2.3k views
in Technique[技术] by (71.8m points)

android - Given byte-array of VectorDrawable, how can I create an instance of VectorDrawable from it?

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.

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

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.


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...