Java – If a class implements a newer interface, Android’s recommended approach to security that supports newer APIs will fail. Why?

If a class implements a newer interface, Android’s recommended approach to security that supports newer APIs will fail. Why?… here is a solution to the problem.

If a class implements a newer interface, Android’s recommended approach to security that supports newer APIs will fail. Why?

To support different API levels, I used the technique described here: http://android-developers.blogspot.com/2010/07/how-to-have-your-cupcake-and-eat-it-too.html

Here is an example from the article:

public static VersionedGestureDetector newInstance(Context context,
        OnGestureListener listener) {
    final int sdkVersion = Integer.parseInt(Build.VERSION.SDK);
    VersionedGestureDetector detector = null;
    if (sdkVersion < Build.VERSION_CODES. ECLAIR) {
        detector = new CupcakeDetector();
    } else if (sdkVersion < Build.VERSION_CODES. FROYO) {
        detector = new EclairDetector();
    } else {
        detector = new FroyoDetector(context);
    }

detector.mListener = listener;

return detector;
}

This approach “takes advantage of the laziness of the classloader.” For devices with newer API levels (Froyo in the example), it can use the Froyo class to access APIs in newer versions. For legacy devices, they receive a class that uses only the old API.

This is very effective.

However, if you ask FroyoDetector to implement an interface, which only exists at the newer api level, when newInstance() is called, or even before it runs any code in the method, it will try to load the interface class implementation of FroyoDetector and put the error in the log stating that the FroyoDetector class could not be loaded.

So my question is, why is this happening? My impression is that with this technique, it won’t load until the first time you directly reference a newer class. However, if you add an interface to it, it seems to try to load it even if detector = new FroyoDetector(context) is not called; Yes.

Here is some code to reproduce the issue:

This is an application for SDK 16 with a minimum value of 8. Running it on 2.3 devices reproduces the issue.

There are three classes here:

public class VersionedLoader {

public static VersionedLoader newInstance() {
        if (Build.VERSION.SDK_INT < 12) {
            return new OldVersionLoader();
        } else {
            return new NewVersionLoader();
        }
    }

}

public class OldVersionLoader extends VersionedLoader {

}

@TargetApi(11)
public class NewVersionLoader extends VersionedLoader implements AnimatorListener {

@Override
    public void onAnimationStart(Animator animation) {}

@Override
    public void onAnimationEnd(Animator animation) {}

@Override
    public void onAnimationCancel(Animator animation) {}

@Override
    public void onAnimationRepeat(Animator animation) {}

}

AnimatorListener is only available after 3.1.

Now, if you run: Object obj = VersionedLoader.newInstance();

This error will appear in the log:

10-27 13:51:14.437: I/dalvikvm(7673): Failed resolving Lyour/package/name/NewVersionLoader; interface 7 'Landroid/animation/Animator$AnimatorListener; '
10-27 13:51:14.437: W/dalvikvm(7673): Link of class 'Lyour/package/name/NewVersionLoader; ' failed
10-27 13:51:14.445: E/dalvikvm(7673): Could not find class 'your.package.name.NewVersionLoader', referenced from method your.package.name.VersionedLoader.newInstance
10-27 13:51:14.445: W/dalvikvm(7673): VFY: unable to resolve new-instance 1327 (Lyour/package/name/NewVersionLoader;) in Lyour/package/name/VersionedLoader;
10-27 13:51:14.445: D/dalvikvm(7673): VFY: replacing opcode 0x22 at 0x000c
10-27 13:51:14.445: D/dalvikvm(7673): VFY: dead code 0x000e-0011 in Lyour/package/name/VersionedLoader;.newInstance ()Lyour/package/name/VersionedLoader;

It will not crash and will continue to work normally.

Solution

Yes, I can reproduce the issue. As you pointed out, though, it’s somewhat surprising that the fact that it doesn’t crash means that this is more of a case where Dalvik might be a bit too talkative in LogCat than anything that should hurt the app.

One workaround is to move the interface to an inner class. In your example, instead of implementing AnimatorListener in NewVersionLoader, the inner class in NewVersionLoader implements AnimationListener:

@TargetApi(11)
public class NewVersionLoader extends VersionedLoader {
    private class Foo implements AnimatorListener {
        @Override
        public void onAnimationStart(Animator animation) {}

@Override
        public void onAnimationEnd(Animator animation) {}

@Override
        public void onAnimationCancel(Animator animation) {}

@Override
        public void onAnimationRepeat(Animator animation) {}

}
}

Admittedly, this may not be ideal, depending on your intended use of VersionedLoader. However, since the VersionedLoader itself does not implement the AnimationListener, the user VersionedLoader will not call the AnimationListener method. So the fact that your logic is on the inner class and not the actual class shouldn’t be a big issue AFAIK.

Related Problems and Solutions