1 /*
2  * Copyright (C) 2014 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.tools.layoutlib.create;
18 
19 import com.android.tools.layoutlib.java.LinkedHashMap_Delegate;
20 import com.android.tools.layoutlib.java.System_Delegate;
21 
22 import org.objectweb.asm.ClassVisitor;
23 import org.objectweb.asm.MethodVisitor;
24 import org.objectweb.asm.Opcodes;
25 import org.objectweb.asm.Type;
26 
27 import java.util.ArrayList;
28 import java.util.Arrays;
29 import java.util.HashSet;
30 import java.util.LinkedHashMap;
31 import java.util.List;
32 import java.util.Locale;
33 import java.util.Map;
34 import java.util.Set;
35 
36 /**
37  * Replaces calls to certain methods that do not exist in the Desktop VM. Useful for methods in the
38  * "java" package.
39  */
40 public class ReplaceMethodCallsAdapter extends ClassVisitor {
41 
42     /**
43      * Descriptors for specialized versions {@link System#arraycopy} that are not present on the
44      * Desktop VM.
45      */
46     private static Set<String> ARRAYCOPY_DESCRIPTORS = new HashSet<>(Arrays.asList(
47             "([CI[CII)V", "([BI[BII)V", "([SI[SII)V", "([II[III)V",
48             "([JI[JII)V", "([FI[FII)V", "([DI[DII)V", "([ZI[ZII)V"));
49 
50     private static final List<MethodReplacer> METHOD_REPLACERS = new ArrayList<>(5);
51 
52     private static final String ANDROID_LOCALE_CLASS =
53             "com/android/layoutlib/bridge/android/AndroidLocale";
54 
55     private static final String JAVA_LOCALE_CLASS = Type.getInternalName(java.util.Locale.class);
56     private static final Type STRING = Type.getType(String.class);
57 
58     private static final String JAVA_LANG_SYSTEM = Type.getInternalName(System.class);
59 
60     // Static initialization block to initialize METHOD_REPLACERS.
61     static {
62         // 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"; } })63         METHOD_REPLACERS.add(new MethodReplacer() {
64             @Override
65             public boolean isNeeded(String owner, String name, String desc, String sourceClass) {
66                 return JAVA_LANG_SYSTEM.equals(owner) && "arraycopy".equals(name) &&
67                         ARRAYCOPY_DESCRIPTORS.contains(desc);
68             }
69 
70             @Override
71             public void replace(MethodInformation mi) {
72                 mi.desc = "(Ljava/lang/Object;ILjava/lang/Object;II)V";
73             }
74         });
75 
76         // 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; } })77         METHOD_REPLACERS.add(new MethodReplacer() {
78 
79             private final String LOCALE_TO_STRING =
80                     Type.getMethodDescriptor(STRING, Type.getType(Locale.class));
81 
82             @Override
83             public boolean isNeeded(String owner, String name, String desc, String sourceClass) {
84                 return JAVA_LOCALE_CLASS.equals(owner) && "()Ljava/lang/String;".equals(desc) &&
85                         ("toLanguageTag".equals(name) || "getScript".equals(name));
86             }
87 
88             @Override
89             public void replace(MethodInformation mi) {
90                 mi.opcode = Opcodes.INVOKESTATIC;
91                 mi.owner = ANDROID_LOCALE_CLASS;
92                 mi.desc = LOCALE_TO_STRING;
93             }
94         });
95 
96         // Case 3: java.util.Locale.adjustLanguageCode() or java.util.Locale.forLanguageTag() or
97         // 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; } })98         METHOD_REPLACERS.add(new MethodReplacer() {
99 
100             private final String STRING_TO_STRING = Type.getMethodDescriptor(STRING, STRING);
101             private final String STRING_TO_LOCALE = Type.getMethodDescriptor(
102                     Type.getType(Locale.class), STRING);
103             private final String VOID_TO_LOCALE =
104                     Type.getMethodDescriptor(Type.getType(Locale.class));
105 
106             @Override
107             public boolean isNeeded(String owner, String name, String desc, String sourceClass) {
108                 return JAVA_LOCALE_CLASS.equals(owner) &&
109                         ("adjustLanguageCode".equals(name) && desc.equals(STRING_TO_STRING) ||
110                         "forLanguageTag".equals(name) && desc.equals(STRING_TO_LOCALE) ||
111                         "getDefault".equals(name) && desc.equals(VOID_TO_LOCALE));
112             }
113 
114             @Override
115             public void replace(MethodInformation mi) {
116                 mi.owner = ANDROID_LOCALE_CLASS;
117             }
118         });
119 
120         // 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); } })121         METHOD_REPLACERS.add(new MethodReplacer() {
122             @Override
123             public boolean isNeeded(String owner, String name, String desc, String sourceClass) {
124                 return JAVA_LANG_SYSTEM.equals(owner) && name.length() == 4
125                         && name.startsWith("log");
126             }
127 
128             @Override
129             public void replace(MethodInformation mi) {
130                 assert mi.desc.equals("(Ljava/lang/String;Ljava/lang/Throwable;)V")
131                         || mi.desc.equals("(Ljava/lang/String;)V");
132                 mi.name = "log";
133                 mi.owner = Type.getInternalName(System_Delegate.class);
134             }
135         });
136 
137         // Case 5: java.lang.System time calls
METHOD_REPLACERS.add(new MethodReplacer() { @Override public boolean isNeeded(String owner, String name, String desc, String sourceClass) { return JAVA_LANG_SYSTEM.equals(owner) && name.equals("nanoTime"); } @Override public void replace(MethodInformation mi) { mi.name = "nanoTime"; mi.owner = Type.getInternalName(System_Delegate.class); } })138         METHOD_REPLACERS.add(new MethodReplacer() {
139             @Override
140             public boolean isNeeded(String owner, String name, String desc, String sourceClass) {
141                 return JAVA_LANG_SYSTEM.equals(owner) && name.equals("nanoTime");
142             }
143 
144             @Override
145             public void replace(MethodInformation mi) {
146                 mi.name = "nanoTime";
147                 mi.owner = Type.getInternalName(System_Delegate.class);
148             }
149         });
METHOD_REPLACERS.add(new MethodReplacer() { @Override public boolean isNeeded(String owner, String name, String desc, String sourceClass) { return JAVA_LANG_SYSTEM.equals(owner) && name.equals("currentTimeMillis"); } @Override public void replace(MethodInformation mi) { mi.name = "currentTimeMillis"; mi.owner = Type.getInternalName(System_Delegate.class); } })150         METHOD_REPLACERS.add(new MethodReplacer() {
151             @Override
152             public boolean isNeeded(String owner, String name, String desc, String sourceClass) {
153                 return JAVA_LANG_SYSTEM.equals(owner) && name.equals("currentTimeMillis");
154             }
155 
156             @Override
157             public void replace(MethodInformation mi) {
158                 mi.name = "currentTimeMillis";
159                 mi.owner = Type.getInternalName(System_Delegate.class);
160             }
161         });
162 
163         // Case 6: 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)); } })164         METHOD_REPLACERS.add(new MethodReplacer() {
165 
166             private final String VOID_TO_MAP_ENTRY =
167                     Type.getMethodDescriptor(Type.getType(Map.Entry.class));
168             private final String LINKED_HASH_MAP = Type.getInternalName(LinkedHashMap.class);
169 
170             @Override
171             public boolean isNeeded(String owner, String name, String desc, String sourceClass) {
172                 return LINKED_HASH_MAP.equals(owner) &&
173                         "eldest".equals(name) &&
174                         VOID_TO_MAP_ENTRY.equals(desc);
175             }
176 
177             @Override
178             public void replace(MethodInformation mi) {
179                 mi.opcode = Opcodes.INVOKESTATIC;
180                 mi.owner = Type.getInternalName(LinkedHashMap_Delegate.class);
181                 mi.desc = Type.getMethodDescriptor(
182                         Type.getType(Map.Entry.class), Type.getType(LinkedHashMap.class));
183             }
184         });
185 
186         // Case 7: android.content.Context.getClassLoader() in LayoutInflater
METHOD_REPLACERS.add(new MethodReplacer() { 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"; } })187         METHOD_REPLACERS.add(new MethodReplacer() {
188             // When LayoutInflater asks for a class loader, we must return the class loader that
189             // cannot return app's custom views/classes. This is so that in case of any failure
190             // or exception when instantiating the views, the IDE can replace it with a mock view
191             // and have proper error handling. However, if a custom view asks for the class
192             // loader, we must return a class loader that can find app's custom views as well.
193             // Thus, we rewrite the call to get class loader in LayoutInflater to
194             // getFrameworkClassLoader and inject a new method in Context. This leaves the normal
195             // method: Context.getClassLoader() free to be used by the apps.
196             private final String VOID_TO_CLASS_LOADER =
197                     Type.getMethodDescriptor(Type.getType(ClassLoader.class));
198 
199             @Override
200             public boolean isNeeded(String owner, String name, String desc, String sourceClass) {
201                 return owner.equals("android/content/Context") &&
202                         sourceClass.equals("android/view/LayoutInflater") &&
203                         name.equals("getClassLoader") &&
204                         desc.equals(VOID_TO_CLASS_LOADER);
205             }
206 
207             @Override
208             public void replace(MethodInformation mi) {
209                 mi.name = "getFrameworkClassLoader";
210             }
211         });
212     }
213 
214     /**
215      * If a method some.package.Class.Method(args) is called from some.other.Class,
216      * @param owner some/package/Class
217      * @param name Method
218      * @param desc (args)returnType
219      * @param sourceClass some/other/Class
220      * @return if the method invocation needs to be replaced by some other class.
221      */
isReplacementNeeded(String owner, String name, String desc, String sourceClass)222     public static boolean isReplacementNeeded(String owner, String name, String desc,
223             String sourceClass) {
224         for (MethodReplacer replacer : METHOD_REPLACERS) {
225             if (replacer.isNeeded(owner, name, desc, sourceClass)) {
226                 return true;
227             }
228         }
229         return false;
230     }
231 
232     private final String mOriginalClassName;
233 
ReplaceMethodCallsAdapter(ClassVisitor cv, String originalClassName)234     public ReplaceMethodCallsAdapter(ClassVisitor cv, String originalClassName) {
235         super(Main.ASM_VERSION, cv);
236         mOriginalClassName = originalClassName;
237     }
238 
239     @Override
visitMethod(int access, String name, String desc, String signature, String[] exceptions)240     public MethodVisitor visitMethod(int access, String name, String desc, String signature,
241             String[] exceptions) {
242         return new MyMethodVisitor(super.visitMethod(access, name, desc, signature, exceptions));
243     }
244 
245     private class MyMethodVisitor extends MethodVisitor {
246 
MyMethodVisitor(MethodVisitor mv)247         public MyMethodVisitor(MethodVisitor mv) {
248             super(Main.ASM_VERSION, mv);
249         }
250 
251         @Override
visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf)252         public void visitMethodInsn(int opcode, String owner, String name, String desc,
253                 boolean itf) {
254             for (MethodReplacer replacer : METHOD_REPLACERS) {
255                 if (replacer.isNeeded(owner, name, desc, mOriginalClassName)) {
256                     MethodInformation mi = new MethodInformation(opcode, owner, name, desc);
257                     replacer.replace(mi);
258                     opcode = mi.opcode;
259                     owner = mi.owner;
260                     name = mi.name;
261                     desc = mi.desc;
262                     break;
263                 }
264             }
265             super.visitMethodInsn(opcode, owner, name, desc, itf);
266         }
267     }
268 
269     private static class MethodInformation {
270         public int opcode;
271         public String owner;
272         public String name;
273         public String desc;
274 
MethodInformation(int opcode, String owner, String name, String desc)275         public MethodInformation(int opcode, String owner, String name, String desc) {
276             this.opcode = opcode;
277             this.owner = owner;
278             this.name = name;
279             this.desc = desc;
280         }
281     }
282 
283     private interface MethodReplacer {
isNeeded(String owner, String name, String desc, String sourceClass)284         boolean isNeeded(String owner, String name, String desc, String sourceClass);
285 
286         /**
287          * Updates the MethodInformation with the new values of the method attributes -
288          * opcode, owner, name and desc.
289          */
replace(MethodInformation mi)290         void replace(MethodInformation mi);
291     }
292 }
293