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