Bytecode Rewriting

TL;DR

We modify the return type of AndroidX's Fragment.getActivity() method from FragmentActivity to Activity to more easily add Fragments from multiple ClassLoaders into the same Fragment tree.

Why?

In Java, two instances of the same class loaded from two different ClassLoaders aren‘t compatible with each other at runtime. Because AndroidX libraries are bundled with each APK that use them, and different APKs are loaded with different ClassLoaders, AndroidX classes from one APK cannot be used with the same class from another APK. This causes problems for Fragment-based UIs in WebLayer, where the implementation is in a different ClassLoader than the embedding app, so its Fragments cannot be added to the embedding app’s Fragment tree.

Note that this issue doesn‘t apply to Framework or standard library classes. Java ClassLoaders form a tree, and if a ClassLoader can’t find a particular class, it delegates to its parent. The leaf ClassLoader used to load an app is responsible for loading the app's class files, while one of its parents will load system-level classes. Because AndroidX classes get loaded by the app-specific ClassLoader, different apps will load mutually incompatible versions, but a class like Activity, which gets loaded from a parent ClassLoader, will be compatible between APKs at runtime, because it ends up gets loaded from a common ClassLoader.

To get around this incompatibility, we can create a RemoteFragment that lives in the embedding app, and a RemoteFragmentImpl that lives in another APK. The RemoteFragment can be added to the original Fragment tree, and will forward all Fragment lifecycle events over an AIDL interface to RemoteFragmentImpl. The fake Fragment in the secondary APK (RemoteFragmentImpl) can create a FragmentController, which allows it to become the host of its own Fragment tree, and any UIs from the secondary ClassLoaded can be added to this new Fragment tree that's been essentially grafted onto the original.

This mostly works, but runs into issues when Fragments call Fragment.getActivity(), which they do a lot. The getActivity implementation takes the Activity given to the FragmentController constructor (via FragmentHostCallback), and casts it to a FragmentActivity before returning it. The original Activity will typically be a FragmentActivity from the embedding app‘s ClassLoader, which means that due to the aforementioned issues, this cast will fail when run in the secondary ClassLoader’s Fragment class because even though the Activity is a FragmentActivity, it's from the wrong ClassLoader.

To fix this second issue, we modify the bytecode of Fragment.getActivity() in the AndroidX prebuilt .aar files to return a plain Activity instead of a FragmentActivity. This allows us to continue calling getActivity() as normal. Note that this does mean FragmentActivity-specific methods can no longer be used in Fragments, but there were no uses of them in Chromium that couldn't be trivially removed as of late 2020.

How does it work?

The bytecode rewriting happens at build time by FragmentActivityReplacer, which is specified as a bytecode rewriter via the bytecode_rewriter_target rule. Compilation errors related to this should get detected by compile_java.py, and print a message pointing users here, which is likely why you're reading this :)

If you need to apply FragmentActivityReplacer to a given target then add …

bytecode_rewriter_target = "//build/android/bytecode:fragment_activity_replacer"

… to the build configuration for that target.

If you still get a build or runtime error related to a FragmentActivity after adding in the replacer, then the library may actually rely on the Activity being a FragmentActivity. If so, it likely won't work with WebLayer as-is. If you know there are no plans to use the library in WebLayer, you can try adding this instead:

bytecode_rewriter_target = "//build/android/bytecode:fragment_activity_replacer_single_androidx"

How does this affect my code?

The goal is for these changes to be as transparent as possible; most code shouldn‘t run into issues. However, if there’s no way around calling a FragmentActivity method in your code, and your Fragment is in //chrome, you could cast the Activity to a FragmentActivity as AndroidX used to do. If your Fragment is in //components, FragmentActivity methods will likely not work directly, and may need to be forwarded to an implementation in the original ClassLoader somehow.

The more important thing to note is that in a multi-ClassLoader world, getActivity() and getContext() will typically return two different objects, so we need to be more careful about which method we call, particularly for code in //components. getActivity() will return the Activity from the original ClassLoader, and should be used to for calls like .finish*(), .setTitle(), and .startActivity()(which live in Activity anyway). When loading resources, you should default to callinggetContext()`, as resources usually come from the same ClassLoader as the Fragment, and the Context should be configured to load them correctly.

As a rule of thumb, prefer getContext() to getActivity(), unless you need to operate on the Activity itself, or you know the resource or setting you need belongs to the original Activity.