blob: a40f39c4ce8dc8597eaec81b70b93acb543f1803 [file] [log] [blame]
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.bytecode;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
import org.objectweb.asm.commons.MethodRemapper;
import org.objectweb.asm.commons.Remapper;
import java.io.File;
import java.io.IOException;
/**
* Java application that modifies Fragment.getActivity() to return an Activity instead of a
* FragmentActivity, and updates any existing getActivity() calls to reference the updated method.
*
* See crbug.com/1144345 for more context.
*/
public class FragmentActivityReplacer extends ByteCodeRewriter {
private static final String GET_ACTIVITY_METHOD_NAME = "getActivity";
private static final String GET_LIFECYCLE_ACTIVITY_METHOD_NAME = "getLifecycleActivity";
private static final String NEW_METHOD_DESCRIPTOR = "()Landroid/app/Activity;";
private static final String OLD_METHOD_DESCRIPTOR =
"()Landroidx/fragment/app/FragmentActivity;";
private static final String REQUIRE_ACTIVITY_METHOD_NAME = "requireActivity";
private static final String SUPPORT_LIFECYCLE_FRAGMENT_IMPL_BINARY_NAME =
"com.google.android.gms.common.api.internal.SupportLifecycleFragmentImpl";
public static void main(String[] args) throws IOException {
// Invoke this script using //build/android/gyp/bytecode_rewriter.py
if (!(args.length == 2 || args.length == 3 && args[0].equals("--single-androidx"))) {
System.err.println("Expected arguments: [--single-androidx] <input.jar> <output.jar>");
System.exit(1);
}
if (args.length == 2) {
FragmentActivityReplacer rewriter = new FragmentActivityReplacer(false);
rewriter.rewrite(new File(args[0]), new File(args[1]));
} else {
FragmentActivityReplacer rewriter = new FragmentActivityReplacer(true);
rewriter.rewrite(new File(args[1]), new File(args[2]));
}
}
private final boolean mSingleAndroidX;
public FragmentActivityReplacer(boolean singleAndroidX) {
mSingleAndroidX = singleAndroidX;
}
@Override
protected boolean shouldRewriteClass(String classPath) {
return true;
}
@Override
protected ClassVisitor getClassVisitorForClass(String classPath, ClassVisitor delegate) {
ClassVisitor invocationVisitor = new InvocationReplacer(delegate, mSingleAndroidX);
switch (classPath) {
case "androidx/fragment/app/Fragment.class":
return new FragmentClassVisitor(invocationVisitor);
case "com/google/android/gms/common/api/internal/SupportLifecycleFragmentImpl.class":
return new SupportLifecycleFragmentImplClassVisitor(invocationVisitor);
default:
return invocationVisitor;
}
}
/**
* Updates any Fragment.getActivity/requireActivity() or getLifecycleActivity() calls to call
* the replaced method.
*/
private static class InvocationReplacer extends ClassVisitor {
private final boolean mSingleAndroidX;
private InvocationReplacer(ClassVisitor baseVisitor, boolean singleAndroidX) {
super(Opcodes.ASM7, baseVisitor);
mSingleAndroidX = singleAndroidX;
}
@Override
public MethodVisitor visitMethod(
int access, String name, String descriptor, String signature, String[] exceptions) {
MethodVisitor base = super.visitMethod(access, name, descriptor, signature, exceptions);
return new MethodVisitor(Opcodes.ASM7, base) {
@Override
public void visitMethodInsn(int opcode, String owner, String name,
String descriptor, boolean isInterface) {
boolean isFragmentGetActivity = name.equals(GET_ACTIVITY_METHOD_NAME)
&& descriptor.equals(OLD_METHOD_DESCRIPTOR)
&& isFragmentSubclass(owner);
boolean isFragmentRequireActivity = name.equals(REQUIRE_ACTIVITY_METHOD_NAME)
&& descriptor.equals(OLD_METHOD_DESCRIPTOR)
&& isFragmentSubclass(owner);
boolean isSupportLifecycleFragmentImplGetLifecycleActivity =
name.equals(GET_LIFECYCLE_ACTIVITY_METHOD_NAME)
&& descriptor.equals(OLD_METHOD_DESCRIPTOR)
&& owner.equals(SUPPORT_LIFECYCLE_FRAGMENT_IMPL_BINARY_NAME);
if ((opcode == Opcodes.INVOKEVIRTUAL || opcode == Opcodes.INVOKESPECIAL)
&& (isFragmentGetActivity || isFragmentRequireActivity
|| isSupportLifecycleFragmentImplGetLifecycleActivity)) {
super.visitMethodInsn(
opcode, owner, name, NEW_METHOD_DESCRIPTOR, isInterface);
if (mSingleAndroidX) {
super.visitTypeInsn(
Opcodes.CHECKCAST, "androidx/fragment/app/FragmentActivity");
}
} else {
super.visitMethodInsn(opcode, owner, name, descriptor, isInterface);
}
}
private boolean isFragmentSubclass(String internalType) {
// Look up classes with a ClassLoader that will resolve any R classes to Object.
// This is fine in this case as resource classes shouldn't be in the class
// hierarchy of any Fragments.
ClassLoader resourceStubbingClassLoader = new ClassLoader() {
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
if (name.matches(".*\\.R(\\$.+)?")) {
return Object.class;
}
return super.findClass(name);
}
};
// This doesn't use Class#isAssignableFrom to avoid us needing to load
// AndroidX's Fragment class, which may not be on the classpath.
try {
String binaryName = Type.getObjectType(internalType).getClassName();
Class<?> clazz = resourceStubbingClassLoader.loadClass(binaryName);
while (clazz != null) {
if (clazz.getName().equals("androidx.fragment.app.Fragment")) {
return true;
}
clazz = clazz.getSuperclass();
}
return false;
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
}
};
}
}
/**
* Updates the implementation of Fragment.getActivity() and Fragment.requireActivity().
*/
private static class FragmentClassVisitor extends ClassVisitor {
private FragmentClassVisitor(ClassVisitor baseVisitor) {
super(Opcodes.ASM7, baseVisitor);
}
@Override
public MethodVisitor visitMethod(
int access, String name, String descriptor, String signature, String[] exceptions) {
// Update the descriptor of getActivity() and requireActivity().
MethodVisitor baseVisitor;
if (descriptor.equals(OLD_METHOD_DESCRIPTOR)
&& (name.equals(GET_ACTIVITY_METHOD_NAME)
|| name.equals(REQUIRE_ACTIVITY_METHOD_NAME))) {
// Some Fragments in a Clank library implement an interface that defines an
// `Activity getActivity()` method. Fragment.getActivity() is considered its
// implementation from a typechecking perspective, but javac still generates a
// getActivity() method in these Fragments that call Fragment.getActivity(). This
// isn't an issue when the methods return different types, but after changing
// Fragment.getActivity() to return an Activity, this generated implementation is
// now overriding Fragment's, which it can't do because Fragment.getActivity() is
// final. We make it non-final here to avoid this issue.
baseVisitor = super.visitMethod(
access & ~Opcodes.ACC_FINAL, name, NEW_METHOD_DESCRIPTOR, null, exceptions);
} else {
baseVisitor = super.visitMethod(access, name, descriptor, signature, exceptions);
}
// Replace getActivity() with `return ContextUtils.activityFromContext(getContext());`
if (name.equals(GET_ACTIVITY_METHOD_NAME) && descriptor.equals(OLD_METHOD_DESCRIPTOR)) {
baseVisitor.visitVarInsn(Opcodes.ALOAD, 0);
baseVisitor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "androidx/fragment/app/Fragment",
"getContext", "()Landroid/content/Context;", false);
baseVisitor.visitMethodInsn(Opcodes.INVOKESTATIC, "org/chromium/utils/ContextUtils",
"activityFromContext", "(Landroid/content/Context;)Landroid/app/Activity;",
false);
baseVisitor.visitInsn(Opcodes.ARETURN);
// Since we set COMPUTE_FRAMES, the arguments of visitMaxs are ignored, but calling
// it forces ClassWriter to actually recompute the correct stack/local values.
// Without this call ClassWriter keeps the original stack=0,locals=1 which is wrong.
baseVisitor.visitMaxs(0, 0);
return null;
}
return new MethodRemapper(baseVisitor, new Remapper() {
@Override
public String mapType(String internalName) {
if (internalName.equals("androidx/fragment/app/FragmentActivity")) {
return "android/app/Activity";
}
return internalName;
}
});
}
}
/**
* Update SupportLifecycleFragmentImpl.getLifecycleActivity().
*/
private static class SupportLifecycleFragmentImplClassVisitor extends ClassVisitor {
private SupportLifecycleFragmentImplClassVisitor(ClassVisitor baseVisitor) {
super(Opcodes.ASM7, baseVisitor);
}
@Override
public MethodVisitor visitMethod(
int access, String name, String descriptor, String signature, String[] exceptions) {
// SupportLifecycleFragmentImpl has two getActivity methods:
// 1. public FragmentActivity getLifecycleActivity():
// This is what you'll see in the source. This delegates to Fragment.getActivity().
// 2. public Activity getLifecycleActivity():
// This is generated because the class implements LifecycleFragment, which
// declares this method, and delegates to #1.
//
// Here we change the return type of #1 and delete #2.
if (name.equals(GET_LIFECYCLE_ACTIVITY_METHOD_NAME)) {
if (descriptor.equals(OLD_METHOD_DESCRIPTOR)) {
return super.visitMethod(
access, name, NEW_METHOD_DESCRIPTOR, signature, exceptions);
}
return null;
}
return super.visitMethod(access, name, descriptor, signature, exceptions);
}
}
}