rovo89 / XposedBridge

The Java part of the Xposed framework.
3.88k stars 1.1k forks source link

BitmapFactory.decodeResource does not respond to resource replacement #65

Open kmark opened 8 years ago

kmark commented 8 years ago

Given the following example resource hook:

xRes.setReplacement(0xdeadbeef, new XResources.DrawableLoader() {
    @Override
    public Drawable newDrawable(XResources xResources, int i) throws Throwable {
        Bitmap b = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888);
        Canvas c = new Canvas(b);
        c.drawColor(Color.RED);
        return new BitmapDrawable(xResources, b);
    }
});

We might expect 0xdeadbeef to now universally refer to a red BitmapDrawable and every instance of the original drawable to be replaced. Akin to physically replacing the image in its drawable-dpi directory. Unfortunately this is not true whenever the resource is accessed via BitmapFactory.decodeResource:

Bitmap b = BitmapFactory.decodeResource(res, 0xdeadbeef);

b contains the original resource not our red square. The following yields the expected replaced Bitmap:

Bitmap b = ((BitmapDrawable)res.getDrawable(0xdeadbeef, context.getTheme())).getBitmap();

The underlying issue is that Resources.getDrawable is extended by XResources to account for replacements where Resources.openRawResource (called by BitmapFactory.decodeResource) is not (#53).

While I'm hesitant to cause this a bug outright, it does make resource replacement inconsistent for a supported resource type.

kmark commented 8 years ago

My current workaround is to just hook decodeResource in the module, check the id, and if it's the one I need to replace feed it with the DrawableBitmap created in the DrawableLoader.

Given the 0xdeadbeef replacement above the following would replace it in decodeResource without duplicating the loader logic:

findAndHookMethod(BitmapFactory.class, "decodeResource", Resources.class, int.class, new XC_MethodHook() {
    @Override
    protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
        int id = (int)param.args[1];
        if(id == 0xdeadbeef) {
            param.setResult(((BitmapDrawable) ((Resources) param.args[0]).getDrawable(id)).getBitmap());
        }
    }
});

Of course if there were an exception or the ID did not match, the hook would fall through and the native decodeResource would be executed. The primary advantage here is that there's no need to get messy with the internals of resources, asset cookies, etc. when the end goal is to just return a Bitmap.

To avoid hardcoding the resource IDs it's possible to run Resources.getIdentifier "once" and then just cache the values in an object field.

yath commented 4 years ago

For reference, to make this work with decodeResource(Resources res, int id, BitmapFactory.Options opts) too, I’m using this based on @kmark’s solution:

XposedHelpers.findAndHookMethod(BitmapFactory.class, "decodeResource", Resources.class, int.class, BitmapFactory.Options.class, new XC_MethodHook() {
    @Override
    protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
        int id = (int)param.args[1];
        if(id == 0xdeadbeef) {
            Resources res = (Resources)param.args[0];
            BitmapFactory.Options options = (BitmapFactory.Options)param.args[2];

            Bitmap b = ((BitmapDrawable)res.getDrawable(id)).getBitmap();

            // We could return b now, except that we’d ignore the options. Therefore, marshal
            // the Bitmap to a PNG byte[] and use BitmapFactory.decodeByteArray() with our
            // caller’s options on this PNG.

            ByteArrayOutputStream s = new ByteArrayOutputStream();
            b.compress(Bitmap.CompressFormat.PNG, 100, s);
            byte[] png = s.toByteArray();

            Bitmap ret = BitmapFactory.decodeByteArray(png, 0, png.length, options);

            param.setResult(ret);
        }
    }
});

This could be made more efficient by storing the byte[] in a static attribute, but decodeResource() in my case is only called once, so I didn’t really bother. :man_shrugging: