| // 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); | |
| } | |
| } | |
| } |