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
?
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.