/* * Copyright (C) 2014 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.tools.layoutlib.create; import com.android.tools.layoutlib.java.LinkedHashMap_Delegate; import com.android.tools.layoutlib.java.System_Delegate; import org.objectweb.asm.ClassVisitor; import org.objectweb.asm.MethodVisitor; import org.objectweb.asm.Opcodes; import org.objectweb.asm.Type; import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; /** * Replaces calls to certain methods that do not exist in the Desktop VM. Useful for methods in the * "java" package. */ public class ReplaceMethodCallsAdapter extends ClassVisitor { /** * Descriptors for specialized versions {@link System#arraycopy} that are not present on the * Desktop VM. */ private static Set<String> ARRAYCOPY_DESCRIPTORS = new HashSet<String>(Arrays.asList( "([CI[CII)V", "([BI[BII)V", "([SI[SII)V", "([II[III)V", "([JI[JII)V", "([FI[FII)V", "([DI[DII)V", "([ZI[ZII)V")); private static final List<MethodReplacer> METHOD_REPLACERS = new ArrayList<MethodReplacer>(5); private static final String ANDROID_LOCALE_CLASS = "com/android/layoutlib/bridge/android/AndroidLocale"; private static final String JAVA_LOCALE_CLASS = Type.getInternalName(java.util.Locale.class); private static final Type STRING = Type.getType(String.class); private static final String JAVA_LANG_SYSTEM = Type.getInternalName(System.class); // Static initialization block to initialize METHOD_REPLACERS. static { // Case 1: java.lang.System.arraycopy() METHOD_REPLACERS.add(new MethodReplacer() { @Override public boolean isNeeded(String owner, String name, String desc, String sourceClass) { return JAVA_LANG_SYSTEM.equals(owner) && "arraycopy".equals(name) && ARRAYCOPY_DESCRIPTORS.contains(desc); } @Override public void replace(MethodInformation mi) { mi.desc = "(Ljava/lang/Object;ILjava/lang/Object;II)V"; } }); // Case 2: java.util.Locale.toLanguageTag() and java.util.Locale.getScript() METHOD_REPLACERS.add(new MethodReplacer() { private final String LOCALE_TO_STRING = Type.getMethodDescriptor(STRING, Type.getType(Locale.class)); @Override public boolean isNeeded(String owner, String name, String desc, String sourceClass) { return JAVA_LOCALE_CLASS.equals(owner) && "()Ljava/lang/String;".equals(desc) && ("toLanguageTag".equals(name) || "getScript".equals(name)); } @Override public void replace(MethodInformation mi) { mi.opcode = Opcodes.INVOKESTATIC; mi.owner = ANDROID_LOCALE_CLASS; mi.desc = LOCALE_TO_STRING; } }); // Case 3: java.util.Locale.adjustLanguageCode() or java.util.Locale.forLanguageTag() or // java.util.Locale.getDefault() METHOD_REPLACERS.add(new MethodReplacer() { private final String STRING_TO_STRING = Type.getMethodDescriptor(STRING, STRING); private final String STRING_TO_LOCALE = Type.getMethodDescriptor( Type.getType(Locale.class), STRING); private final String VOID_TO_LOCALE = Type.getMethodDescriptor(Type.getType(Locale.class)); @Override public boolean isNeeded(String owner, String name, String desc, String sourceClass) { return JAVA_LOCALE_CLASS.equals(owner) && ("adjustLanguageCode".equals(name) && desc.equals(STRING_TO_STRING) || "forLanguageTag".equals(name) && desc.equals(STRING_TO_LOCALE) || "getDefault".equals(name) && desc.equals(VOID_TO_LOCALE)); } @Override public void replace(MethodInformation mi) { mi.owner = ANDROID_LOCALE_CLASS; } }); // Case 4: java.lang.System.log?() METHOD_REPLACERS.add(new MethodReplacer() { @Override public boolean isNeeded(String owner, String name, String desc, String sourceClass) { return JAVA_LANG_SYSTEM.equals(owner) && name.length() == 4 && name.startsWith("log"); } @Override public void replace(MethodInformation mi) { assert mi.desc.equals("(Ljava/lang/String;Ljava/lang/Throwable;)V") || mi.desc.equals("(Ljava/lang/String;)V"); mi.name = "log"; mi.owner = Type.getInternalName(System_Delegate.class); } }); // Case 5: java.util.LinkedHashMap.eldest() METHOD_REPLACERS.add(new MethodReplacer() { private final String VOID_TO_MAP_ENTRY = Type.getMethodDescriptor(Type.getType(Map.Entry.class)); private final String LINKED_HASH_MAP = Type.getInternalName(LinkedHashMap.class); @Override public boolean isNeeded(String owner, String name, String desc, String sourceClass) { return LINKED_HASH_MAP.equals(owner) && "eldest".equals(name) && VOID_TO_MAP_ENTRY.equals(desc); } @Override public void replace(MethodInformation mi) { mi.opcode = Opcodes.INVOKESTATIC; mi.owner = Type.getInternalName(LinkedHashMap_Delegate.class); mi.desc = Type.getMethodDescriptor( Type.getType(Map.Entry.class), Type.getType(LinkedHashMap.class)); } }); // Case 6: android.content.Context.getClassLoader() in LayoutInflater METHOD_REPLACERS.add(new MethodReplacer() { // When LayoutInflater asks for a class loader, we must return the class loader that // cannot return app's custom views/classes. This is so that in case of any failure // or exception when instantiating the views, the IDE can replace it with a mock view // and have proper error handling. However, if a custom view asks for the class // loader, we must return a class loader that can find app's custom views as well. // Thus, we rewrite the call to get class loader in LayoutInflater to // getFrameworkClassLoader and inject a new method in Context. This leaves the normal // method: Context.getClassLoader() free to be used by the apps. private final String VOID_TO_CLASS_LOADER = Type.getMethodDescriptor(Type.getType(ClassLoader.class)); @Override public boolean isNeeded(String owner, String name, String desc, String sourceClass) { return owner.equals("android/content/Context") && sourceClass.equals("android/view/LayoutInflater") && name.equals("getClassLoader") && desc.equals(VOID_TO_CLASS_LOADER); } @Override public void replace(MethodInformation mi) { mi.name = "getFrameworkClassLoader"; } }); } /** * If a method some.package.Class.Method(args) is called from some.other.Class, * @param owner some/package/Class * @param name Method * @param desc (args)returnType * @param sourceClass some/other/Class * @return if the method invocation needs to be replaced by some other class. */ public static boolean isReplacementNeeded(String owner, String name, String desc, String sourceClass) { for (MethodReplacer replacer : METHOD_REPLACERS) { if (replacer.isNeeded(owner, name, desc, sourceClass)) { return true; } } return false; } private final String mOriginalClassName; public ReplaceMethodCallsAdapter(ClassVisitor cv, String originalClassName) { super(Opcodes.ASM4, cv); mOriginalClassName = originalClassName; } @Override public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) { return new MyMethodVisitor(super.visitMethod(access, name, desc, signature, exceptions)); } private class MyMethodVisitor extends MethodVisitor { public MyMethodVisitor(MethodVisitor mv) { super(Opcodes.ASM4, mv); } @Override public void visitMethodInsn(int opcode, String owner, String name, String desc) { for (MethodReplacer replacer : METHOD_REPLACERS) { if (replacer.isNeeded(owner, name, desc, mOriginalClassName)) { MethodInformation mi = new MethodInformation(opcode, owner, name, desc); replacer.replace(mi); opcode = mi.opcode; owner = mi.owner; name = mi.name; desc = mi.desc; break; } } super.visitMethodInsn(opcode, owner, name, desc); } } private static class MethodInformation { public int opcode; public String owner; public String name; public String desc; public MethodInformation(int opcode, String owner, String name, String desc) { this.opcode = opcode; this.owner = owner; this.name = name; this.desc = desc; } } private interface MethodReplacer { boolean isNeeded(String owner, String name, String desc, String sourceClass); /** * Updates the MethodInformation with the new values of the method attributes - * opcode, owner, name and desc. */ void replace(MethodInformation mi); } }