blob: a6ae4443de19764aa8fd17fccf5f97d61918fce6 [file] [log] [blame]
// Copyright 2017 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 static org.objectweb.asm.Opcodes.ACC_PUBLIC;
import static org.objectweb.asm.Opcodes.ACONST_NULL;
import static org.objectweb.asm.Opcodes.ALOAD;
import static org.objectweb.asm.Opcodes.ARETURN;
import static org.objectweb.asm.Opcodes.ASM7;
import static org.objectweb.asm.Opcodes.BIPUSH;
import static org.objectweb.asm.Opcodes.GETSTATIC;
import static org.objectweb.asm.Opcodes.IFNE;
import static org.objectweb.asm.Opcodes.IF_ICMPGE;
import static org.objectweb.asm.Opcodes.ILOAD;
import static org.objectweb.asm.Opcodes.INVOKESPECIAL;
import static org.objectweb.asm.Opcodes.INVOKESTATIC;
import static org.objectweb.asm.Opcodes.INVOKEVIRTUAL;
import static org.objectweb.asm.Opcodes.RETURN;
import static org.chromium.bytecode.TypeUtils.ASSET_MANAGER;
import static org.chromium.bytecode.TypeUtils.BOOLEAN;
import static org.chromium.bytecode.TypeUtils.BUILD_HOOKS_ANDROID;
import static org.chromium.bytecode.TypeUtils.CONFIGURATION;
import static org.chromium.bytecode.TypeUtils.CONTEXT;
import static org.chromium.bytecode.TypeUtils.CONTEXT_WRAPPER;
import static org.chromium.bytecode.TypeUtils.INT;
import static org.chromium.bytecode.TypeUtils.RESOURCES;
import static org.chromium.bytecode.TypeUtils.STRING;
import static org.chromium.bytecode.TypeUtils.THEME;
import static org.chromium.bytecode.TypeUtils.VOID;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.Label;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import java.util.Arrays;
import java.util.List;
/**
* A ClassVisitor for providing access to custom resources via BuildHooksAndroid.
*
* The goal of this class is to provide hooks into all places where android resources
* are available so that they can be modified before use. This is done by rewriting the bytecode
* for all callable definitions of certain Context methods, specifically:
* - getResources
* - getAssets
* - getTheme
* - setTheme
* - createConfigurationContext
*
* Only classes at the framework boundary are rewritten since presumably all other indirect Context
* subclasses will end up calling their respective super methods (i.e. we bytecode rewrite
* BaseChromiumApplication since it extends Application, but not ContentApplication since it
* extends a non-framework subclass.
*/
class CustomResourcesClassAdapter extends ClassVisitor {
private static final String IS_ENABLED_METHOD = "isEnabled";
private static final String IS_ENABLED_DESCRIPTOR = TypeUtils.getMethodDescriptor(BOOLEAN);
// Cached since this is used so often.
private static final String GET_IDENTIFIER_DESCRIPTOR =
TypeUtils.getMethodDescriptor(INT, STRING, STRING, STRING);
// Existing methods are more difficult to handle, and not currently needed.
private static final List<String> PROHIBITED_METHODS = Arrays.asList(
TypeUtils.getMethodSignature("getResources", RESOURCES),
TypeUtils.getMethodSignature("getAssets", ASSET_MANAGER),
TypeUtils.getMethodSignature("getTheme", THEME),
TypeUtils.getMethodSignature("createConfigurationContext", CONTEXT, CONFIGURATION),
TypeUtils.getMethodSignature("setTheme", VOID, INT));
private boolean mShouldTransform;
private String mClassName;
private String mSuperClassName;
private ClassLoader mClassLoader;
CustomResourcesClassAdapter(ClassVisitor visitor, String className, String superClassName,
ClassLoader classLoader) {
super(ASM7, visitor);
this.mClassName = className;
this.mSuperClassName = superClassName;
this.mClassLoader = classLoader;
}
@Override
public void visit(int version, int access, String name, String signature, String superName,
String[] interfaces) {
super.visit(version, access, name, signature, superName, interfaces);
mShouldTransform = shouldTransform();
}
@Override
public MethodVisitor visitMethod(final int access, final String name, String desc,
String signature, String[] exceptions) {
if (mShouldTransform) {
String methodSignature = name + desc;
if (requiresModifyingExisting(methodSignature)) {
throw new RuntimeException("Rewriting existing methods not supported: " + mClassName
+ "#" + methodSignature);
}
}
return new RewriteGetIdentifierMethodVisitor(
super.visitMethod(access, name, desc, signature, exceptions));
}
@Override
public void visitEnd() {
if (mShouldTransform) {
delegateCreateConfigurationContext();
delegateSetTheme();
delegateGet("getAssets", ASSET_MANAGER);
delegateGet("getTheme", THEME);
delegateGet("getResources", RESOURCES);
}
super.visitEnd();
}
private boolean requiresModifyingExisting(String methodDescriptor) {
return PROHIBITED_METHODS.contains(methodDescriptor);
}
private boolean shouldTransform() {
if (!isDescendantOfContext()) {
return false;
}
if (!superClassIsFrameworkClass()) {
return false;
}
return !superClassIsContextWrapper();
}
private boolean superClassIsFrameworkClass() {
return loadClass(mSuperClassName).getProtectionDomain().toString().contains("android.jar");
}
private boolean isDescendantOfContext() {
return isSubClass(mClassName, CONTEXT);
}
private boolean superClassIsContextWrapper() {
return mSuperClassName.equals(CONTEXT_WRAPPER);
}
private boolean isSubClass(String candidate, String other) {
Class<?> candidateClazz = loadClass(candidate);
Class<?> parentClazz = loadClass(other);
return parentClazz.isAssignableFrom(candidateClazz);
}
private Class<?> loadClass(String className) {
try {
return mClassLoader.loadClass(className.replace('/', '.'));
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
}
/**
* Remaps Resources.getIdentifier() method calls to use BuildHooksAndroid.
*
* resourceObj.getIdentifier(String, String, String) becomes:
* BuildHooksAndroid.getIdentifier(resourceObj, String, String, String);
*/
private static final class RewriteGetIdentifierMethodVisitor extends MethodVisitor {
RewriteGetIdentifierMethodVisitor(MethodVisitor mv) {
super(ASM7, mv);
}
@Override
public void visitMethodInsn(
int opcode, String owner, String name, String desc, boolean itf) {
String methodName = "getIdentifier";
if (opcode == INVOKEVIRTUAL && owner.equals(RESOURCES) && name.equals(methodName)
&& desc.equals(GET_IDENTIFIER_DESCRIPTOR)) {
super.visitMethodInsn(INVOKESTATIC, BUILD_HOOKS_ANDROID, methodName,
TypeUtils.getMethodDescriptor(INT, RESOURCES, STRING, STRING, STRING), itf);
} else {
super.visitMethodInsn(opcode, owner, name, desc, itf);
}
}
}
/**
* Generates:
*
* <pre>
* public Context createConfigurationContext(Configuration configuration) {
* // createConfigurationContext does not exist before API level 17.
* if (Build.VERSION.SDK_INT < 17) return null;
* if (!BuildHooksAndroid.isEnabled()) return super.createConfigurationContext(configuration);
* return BuildHooksAndroid.createConfigurationContext(
* super.createConfigurationContext(configuration));
* }
* </pre>
* }
*/
private void delegateCreateConfigurationContext() {
String methodName = "createConfigurationContext";
String methodDescriptor = TypeUtils.getMethodDescriptor(CONTEXT, CONFIGURATION);
MethodVisitor mv = super.visitMethod(ACC_PUBLIC, methodName, methodDescriptor, null, null);
mv.visitCode();
mv.visitFieldInsn(GETSTATIC, "android/os/Build$VERSION", "SDK_INT", INT);
mv.visitIntInsn(BIPUSH, 17);
Label l0 = new Label();
mv.visitJumpInsn(IF_ICMPGE, l0);
mv.visitInsn(ACONST_NULL);
mv.visitInsn(ARETURN);
mv.visitLabel(l0);
mv.visitFrame(Opcodes.F_SAME, 0, null, 0, null);
mv.visitMethodInsn(
INVOKESTATIC, BUILD_HOOKS_ANDROID, IS_ENABLED_METHOD, IS_ENABLED_DESCRIPTOR, false);
Label l1 = new Label();
mv.visitJumpInsn(IFNE, l1);
mv.visitVarInsn(ALOAD, 0);
mv.visitVarInsn(ALOAD, 1);
mv.visitMethodInsn(INVOKESPECIAL, mSuperClassName, methodName, methodDescriptor, false);
mv.visitInsn(ARETURN);
mv.visitLabel(l1);
mv.visitFrame(Opcodes.F_SAME, 0, null, 0, null);
mv.visitVarInsn(ALOAD, 0);
mv.visitVarInsn(ALOAD, 1);
mv.visitMethodInsn(INVOKESPECIAL, mSuperClassName, methodName, methodDescriptor, false);
mv.visitMethodInsn(INVOKESTATIC, BUILD_HOOKS_ANDROID, methodName,
TypeUtils.getMethodDescriptor(CONTEXT, CONTEXT), false);
mv.visitInsn(ARETURN);
mv.visitMaxs(2, 2);
mv.visitEnd();
}
/**
* Generates:
*
* <pre>
* public void setTheme(int theme) {
* if (!BuildHooksAndroid.isEnabled()) {
* super.setTheme(theme);
* return;
* }
* BuildHooksAndroid.setTheme(this, theme);
* }
* </pre>
*/
private void delegateSetTheme() {
String methodName = "setTheme";
String methodDescriptor = TypeUtils.getMethodDescriptor(VOID, INT);
String buildHooksMethodDescriptor = TypeUtils.getMethodDescriptor(VOID, CONTEXT, INT);
MethodVisitor mv = super.visitMethod(ACC_PUBLIC, methodName, methodDescriptor, null, null);
mv.visitCode();
mv.visitMethodInsn(
INVOKESTATIC, BUILD_HOOKS_ANDROID, IS_ENABLED_METHOD, IS_ENABLED_DESCRIPTOR, false);
Label l0 = new Label();
mv.visitJumpInsn(IFNE, l0);
mv.visitVarInsn(ALOAD, 0);
mv.visitVarInsn(ILOAD, 1);
mv.visitMethodInsn(INVOKESPECIAL, mSuperClassName, methodName, methodDescriptor, false);
mv.visitInsn(RETURN);
mv.visitLabel(l0);
mv.visitFrame(Opcodes.F_SAME, 0, null, 0, null);
mv.visitVarInsn(ALOAD, 0);
mv.visitVarInsn(ILOAD, 1);
mv.visitMethodInsn(
INVOKESTATIC, BUILD_HOOKS_ANDROID, methodName, buildHooksMethodDescriptor, false);
mv.visitInsn(RETURN);
mv.visitMaxs(2, 2);
mv.visitEnd();
}
/**
* Generates:
*
* <pre>
* public returnType methodName() {
* if (!BuildHooksAndroid.isEnabled()) return super.methodName();
* return BuildHooksAndroid.methodName(this);
* }
* </pre>
*/
private void delegateGet(String methodName, String returnType) {
String getMethodDescriptor = TypeUtils.getMethodDescriptor(returnType);
String buildHooksGetMethodDescriptor = TypeUtils.getMethodDescriptor(returnType, CONTEXT);
MethodVisitor mv =
super.visitMethod(ACC_PUBLIC, methodName, getMethodDescriptor, null, null);
mv.visitCode();
mv.visitMethodInsn(
INVOKESTATIC, BUILD_HOOKS_ANDROID, IS_ENABLED_METHOD, IS_ENABLED_DESCRIPTOR, false);
Label l0 = new Label();
mv.visitJumpInsn(IFNE, l0);
mv.visitVarInsn(ALOAD, 0);
mv.visitMethodInsn(INVOKESPECIAL, mSuperClassName, methodName, getMethodDescriptor, false);
mv.visitInsn(ARETURN);
mv.visitLabel(l0);
mv.visitFrame(Opcodes.F_SAME, 0, null, 0, null);
mv.visitVarInsn(ALOAD, 0);
mv.visitMethodInsn(INVOKESTATIC, BUILD_HOOKS_ANDROID, methodName,
buildHooksGetMethodDescriptor, false);
mv.visitInsn(ARETURN);
mv.visitMaxs(1, 1);
mv.visitEnd();
}
}