Search code examples
androidbyte-buddy

How to replace an Android Activity with ByteBuddy


I am attempting to develop an Android Application that supports dynamic activities.

As all Activities need to be declared in the Manifest XML I have created a "dummy" activity that does nothing.

   <activity
    android:name="com.research.Dummy"
    android:label="@string/title_activity_dummy"
    android:theme="@style/AppTheme.NoActionBar" />

What I wish to achieve is that using ByteBuddy I instantiate a new version of Dummy that extends different superclasses and/or implements one or more interfaces.

I have this code to test extending a superclass:-

    final Class<? extends SomeClass> dynamicType = new ByteBuddy(ClassFileVersion.JAVA_V8)                    .subclass(com.pspdfkit.ui.PdfActivity.class, IMITATE_SUPER_CLASS)
    .name("com.research.Dummy")
    .method(ElementMatchers.named("toString"))
    .intercept(FixedValue.value("Hello World!"))
    .make()
    .load(getClass().getClassLoader(), new AndroidClassLoadingStrategy.Wrapping(this.getDir("dexgen", Context.MODE_PRIVATE))).getLoaded();

final Intent intent = new Intent(this, dynamicType);
startActivity(intent);

My Dummy activity extends android.support.v7.app.AppCompatActivity, however, I want my ByteBuddy version to extend "SomeClass".

The above code doesn't give me the desired effect, Dummy activity does display however it extends android.support.v7.app.AppCompatActivity and not SomeClass.

Can ByteBuddy replace the original Dummy class that's extending AppCompatActivity with the required version that's extending SomeClass?

UPDATE

When I try this to inject my new class:-

final File jarFile = new File(getFilesDir(), "buddyDummy.jar");

final DynamicType.Unloaded<? extends AppCompatActivity> dynamicType = new ByteBuddy()
        .subclass(AppCompatActivity.class)
        .name("com.research.buddy.Dynamic")
        .make();

final File dexInternalStoragePath = dynamicType.toJar(jarFile);

// Internal storage where the DexClassLoader writes the optimized dex file to.
final File optimizedDexOutputPath = getDir("outdex", Context.MODE_PRIVATE);

// Initialize the class loader with the secondary dex file.
final DexClassLoader dexClassLoader = new DexClassLoader(dexInternalStoragePath.getAbsolutePath(), optimizedDexOutputPath.getAbsolutePath(),null, getClassLoader());
dexClassLoader.loadClass("com.research.buddy.Dynamic");

I receive this exception

java.lang.ClassNotFoundException: Didn't find class "com.research.buddy.Dynamic" on path: 
DexPathList[[zip file "/data/user/0/com.research.buddy/files/buddyDummy.jar"],nativeLibraryDirectories=[/vendor/lib, /system/lib]]
   at dalvik.system.BaseDexClassLoader.findClass(BaseDexClassLoader.java:56)
   at java.lang.ClassLoader.loadClass(ClassLoader.java:511)
   at java.lang.ClassLoader.loadClass(ClassLoader.java:469)
   at com.research.buddy.Buddy.findRuntimeDependencies(Buddy.java:117)
   at com.research.buddy.Buddy.onClick(Buddy.java:78)
   at android.view.View.performClick(View.java:5204)
   at android.view.View$PerformClick.run(View.java:21153)
   at android.os.Handler.handleCallback(Handler.java:739)
   at android.os.Handler.dispatchMessage(Handler.java:95)
   at android.os.Looper.loop(Looper.java:148)
   at android.app.ActivityThread.main(ActivityThread.java:5417)
   at java.lang.reflect.Method.invoke(Native Method)
   at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:726)
   at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:616)
 Suppressed: java.io.IOException: No original dex files found for dex location /data/user/0/com.research.buddy/files/buddyDummy.jar
   at dalvik.system.DexFile.openDexFileNative(Native Method)
   at dalvik.system.DexFile.openDexFile(DexFile.java:295)
   at dalvik.system.DexFile.<init>(DexFile.java:111)
   at dalvik.system.DexFile.loadDex(DexFile.java:151)
   at dalvik.system.DexPathList.loadDexFile(DexPathList.java:282)
   at dalvik.system.DexPathList.makePathElements(DexPathList.java:248)
   at dalvik.system.DexPathList.<init>(DexPathList.java:120)
   at dalvik.system.BaseDexClassLoader.<init>(BaseDexClassLoader.java:48)
   at dalvik.system.DexClassLoader.<init>(DexClassLoader.java:57)
   at com.research.buddy.Buddy.findRuntimeDependencies(Buddy.java:116)
         ... 10 more
 Suppressed: java.lang.ClassNotFoundException: Didn't find class "com.research.buddy.Dynamic" on path: 
 DexPathList[[zip file "/data/app/com.research.buddy-1/base.apk"],nativeLibraryDirectories=[/data/app/com.research.buddy-1/lib/arm, /vendor/lib, /system/lib]]
   at dalvik.system.BaseDexClassLoader.findClass(BaseDexClassLoader.java:56)
   at java.lang.ClassLoader.loadClass(ClassLoader.java:511)
   at java.lang.ClassLoader.loadClass(ClassLoader.java:504)
         ... 12 more
     Suppressed: java.lang.ClassNotFoundException: com.research.buddy.Dynamic
   at java.lang.Class.classForName(Native Method)
   at java.lang.BootClassLoader.findClass(ClassLoader.java:781)
   at java.lang.BootClassLoader.loadClass(ClassLoader.java:841)
   at java.lang.ClassLoader.loadClass(ClassLoader.java:504)
             ... 13 more
Caused by: java.lang.NoClassDefFoundError: Class not found using the boot class loader; no stack trace available

When I look at the file location on my device I can see the buddyDummy.jar file at the correct location.

Why am I getting a ClassNotFoundException?

UPDATE

I have managed to instantiate my Android Activity with ByteBuddy and "startActivity" it with the following code.

final DynamicType.Unloaded<? extends AppCompatActivity> dynamicType = new ByteBuddy()
        .subclass(AppCompatActivity.class)
        .name(CLASS_NAME)
        .make();

final Class<? extends AppCompatActivity> dynamicTypeClass = dynamicType.load(getClassLoader(), new AndroidClassLoadingStrategy.Injecting(this.getDir("dexgen", Context.MODE_PRIVATE))).getLoaded();

final Intent intent = new Intent(this, dynamicTypeClass);
startActivity(intent);

I now need to intercept the onCreate method to allow me to set the content.

This step seems to have a number of issues, namely onCreate is a protected method and I need to call the super classes onCreate method passing the Bundle.

Is it possible to intercept protected methods with ByteBuddy? How can I call super.onCreate(savedInstance) within my method interceptor?

@Super, @SuperCall etc just have call() e.g. no args, what am I missing?

UPDATE

I have managed to successfully display my Dynamic activity, set the required layout and call super.onCreate(). Although I believe the super call is out of sequence.

final DynamicType.Unloaded<? extends AppCompatActivity> dynamicType = new ByteBuddy(ClassFileVersion.JAVA_V8)
        .subclass(AppCompatActivity.class)
        .name(CLASS_NAME)
        .method(named("onCreate").and(takesArguments(1)))
        .intercept(MethodDelegation.to(TargetActivity.class).andThen(SuperMethodCall.INSTANCE))
        .make();

final Class<? extends AppCompatActivity> dynamicTypeClass = dynamicType.load(getClassLoader(), new AndroidClassLoadingStrategy.Injecting(this.getDir("dexgen", Context.MODE_PRIVATE))).getLoaded();

final Intent intent = new Intent(this, dynamicTypeClass);
startActivity(intent);

My TargetActivity resembles this:-

public class TargetActivity {
    public static void intercept(Bundle savedInstanceState, @This AppCompatActivity thiz) {
        thiz.setContentView(R.layout.activity_fourth);
    }
}

As my MethodDelegation call to SuperMethod is ".andThen" it makes it sounds like the super.onCreate() is called after I have set content, how do I call Super .previous and not .andThen?


Solution

  • You are using the Wrapping strategy where the class is loaded into a new class loader. If you want to load a class instead of another class, you have to inject it.

    This pattern can however be awkward as you are making an assumption that you get to inject the class before the original class is loaded what is often dependant on details of the executing JVM.

    As for your question update: Did you specify the right parent class loader? Instead of getClass().getClassLoader() try com.research.buddy.Dynamic.class.getClassLoader(). It seems you are loading the class into the wrong scope.