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
17package com.android.tools.layoutlib.create;
18
19import com.android.tools.layoutlib.java.LinkedHashMap_Delegate;
20import com.android.tools.layoutlib.java.System_Delegate;
21
22import org.objectweb.asm.ClassVisitor;
23import org.objectweb.asm.MethodVisitor;
24import org.objectweb.asm.Opcodes;
25import org.objectweb.asm.Type;
26
27import java.util.ArrayList;
28import java.util.Arrays;
29import java.util.HashSet;
30import java.util.LinkedHashMap;
31import java.util.List;
32import java.util.Locale;
33import java.util.Map;
34import 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 */
40public 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()
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()
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()
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?()
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
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        });
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()
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
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     */
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
234    public ReplaceMethodCallsAdapter(ClassVisitor cv, String originalClassName) {
235        super(Main.ASM_VERSION, cv);
236        mOriginalClassName = originalClassName;
237    }
238
239    @Override
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
247        public MyMethodVisitor(MethodVisitor mv) {
248            super(Main.ASM_VERSION, mv);
249        }
250
251        @Override
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
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 {
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         */
290        void replace(MethodInformation mi);
291    }
292}
293