1/*
2 * Copyright (C) 2013 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 android.ddm;
18
19import android.util.Log;
20import android.view.View;
21import android.view.ViewDebug;
22import android.view.ViewRootImpl;
23import android.view.WindowManagerGlobal;
24
25import org.apache.harmony.dalvik.ddmc.Chunk;
26import org.apache.harmony.dalvik.ddmc.ChunkHandler;
27import org.apache.harmony.dalvik.ddmc.DdmServer;
28
29import java.io.BufferedWriter;
30import java.io.ByteArrayOutputStream;
31import java.io.DataOutputStream;
32import java.io.IOException;
33import java.io.OutputStreamWriter;
34import java.lang.reflect.Method;
35import java.nio.BufferUnderflowException;
36import java.nio.ByteBuffer;
37
38/**
39 * Handle various requests related to profiling / debugging of the view system.
40 * Support for these features are advertised via {@link DdmHandleHello}.
41 */
42public class DdmHandleViewDebug extends ChunkHandler {
43    /** List {@link ViewRootImpl}'s of this process. */
44    private static final int CHUNK_VULW = type("VULW");
45
46    /** Operation on view root, first parameter in packet should be one of VURT_* constants */
47    private static final int CHUNK_VURT = type("VURT");
48
49    /** Dump view hierarchy. */
50    private static final int VURT_DUMP_HIERARCHY = 1;
51
52    /** Capture View Layers. */
53    private static final int VURT_CAPTURE_LAYERS = 2;
54
55    /** Dump View Theme. */
56    private static final int VURT_DUMP_THEME = 3;
57
58    /**
59     * Generic View Operation, first parameter in the packet should be one of the
60     * VUOP_* constants below.
61     */
62    private static final int CHUNK_VUOP = type("VUOP");
63
64    /** Capture View. */
65    private static final int VUOP_CAPTURE_VIEW = 1;
66
67    /** Obtain the Display List corresponding to the view. */
68    private static final int VUOP_DUMP_DISPLAYLIST = 2;
69
70    /** Profile a view. */
71    private static final int VUOP_PROFILE_VIEW = 3;
72
73    /** Invoke a method on the view. */
74    private static final int VUOP_INVOKE_VIEW_METHOD = 4;
75
76    /** Set layout parameter. */
77    private static final int VUOP_SET_LAYOUT_PARAMETER = 5;
78
79    /** Error code indicating operation specified in chunk is invalid. */
80    private static final int ERR_INVALID_OP = -1;
81
82    /** Error code indicating that the parameters are invalid. */
83    private static final int ERR_INVALID_PARAM = -2;
84
85    /** Error code indicating an exception while performing operation. */
86    private static final int ERR_EXCEPTION = -3;
87
88    private static final String TAG = "DdmViewDebug";
89
90    private static final DdmHandleViewDebug sInstance = new DdmHandleViewDebug();
91
92    /** singleton, do not instantiate. */
93    private DdmHandleViewDebug() {}
94
95    public static void register() {
96        DdmServer.registerHandler(CHUNK_VULW, sInstance);
97        DdmServer.registerHandler(CHUNK_VURT, sInstance);
98        DdmServer.registerHandler(CHUNK_VUOP, sInstance);
99    }
100
101    @Override
102    public void connected() {
103    }
104
105    @Override
106    public void disconnected() {
107    }
108
109    @Override
110    public Chunk handleChunk(Chunk request) {
111        int type = request.type;
112
113        if (type == CHUNK_VULW) {
114            return listWindows();
115        }
116
117        ByteBuffer in = wrapChunk(request);
118        int op = in.getInt();
119
120        View rootView = getRootView(in);
121        if (rootView == null) {
122            return createFailChunk(ERR_INVALID_PARAM, "Invalid View Root");
123        }
124
125        if (type == CHUNK_VURT) {
126            if (op == VURT_DUMP_HIERARCHY)
127                return dumpHierarchy(rootView, in);
128            else if (op == VURT_CAPTURE_LAYERS)
129                return captureLayers(rootView);
130            else if (op == VURT_DUMP_THEME)
131                return dumpTheme(rootView);
132            else
133                return createFailChunk(ERR_INVALID_OP, "Unknown view root operation: " + op);
134        }
135
136        final View targetView = getTargetView(rootView, in);
137        if (targetView == null) {
138            return createFailChunk(ERR_INVALID_PARAM, "Invalid target view");
139        }
140
141        if (type == CHUNK_VUOP) {
142            switch (op) {
143                case VUOP_CAPTURE_VIEW:
144                    return captureView(rootView, targetView);
145                case VUOP_DUMP_DISPLAYLIST:
146                    return dumpDisplayLists(rootView, targetView);
147                case VUOP_PROFILE_VIEW:
148                    return profileView(rootView, targetView);
149                case VUOP_INVOKE_VIEW_METHOD:
150                    return invokeViewMethod(rootView, targetView, in);
151                case VUOP_SET_LAYOUT_PARAMETER:
152                    return setLayoutParameter(rootView, targetView, in);
153                default:
154                    return createFailChunk(ERR_INVALID_OP, "Unknown view operation: " + op);
155            }
156        } else {
157            throw new RuntimeException("Unknown packet " + ChunkHandler.name(type));
158        }
159    }
160
161    /** Returns the list of windows owned by this client. */
162    private Chunk listWindows() {
163        String[] windowNames = WindowManagerGlobal.getInstance().getViewRootNames();
164
165        int responseLength = 4;                     // # of windows
166        for (String name : windowNames) {
167            responseLength += 4;                    // length of next window name
168            responseLength += name.length() * 2;    // window name
169        }
170
171        ByteBuffer out = ByteBuffer.allocate(responseLength);
172        out.order(ChunkHandler.CHUNK_ORDER);
173
174        out.putInt(windowNames.length);
175        for (String name : windowNames) {
176            out.putInt(name.length());
177            putString(out, name);
178        }
179
180        return new Chunk(CHUNK_VULW, out);
181    }
182
183    private View getRootView(ByteBuffer in) {
184        try {
185            int viewRootNameLength = in.getInt();
186            String viewRootName = getString(in, viewRootNameLength);
187            return WindowManagerGlobal.getInstance().getRootView(viewRootName);
188        } catch (BufferUnderflowException e) {
189            return null;
190        }
191    }
192
193    private View getTargetView(View root, ByteBuffer in) {
194        int viewLength;
195        String viewName;
196
197        try {
198            viewLength = in.getInt();
199            viewName = getString(in, viewLength);
200        } catch (BufferUnderflowException e) {
201            return null;
202        }
203
204        return ViewDebug.findView(root, viewName);
205    }
206
207    /**
208     * Returns the view hierarchy and/or view properties starting at the provided view.
209     * Based on the input options, the return data may include:
210     *  - just the view hierarchy
211     *  - view hierarchy & the properties for each of the views
212     *  - just the view properties for a specific view.
213     *  TODO: Currently this only returns views starting at the root, need to fix so that
214     *  it can return properties of any view.
215     */
216    private Chunk dumpHierarchy(View rootView, ByteBuffer in) {
217        boolean skipChildren = in.getInt() > 0;
218        boolean includeProperties = in.getInt() > 0;
219        boolean v2 = in.hasRemaining() && in.getInt() > 0;
220
221        long start = System.currentTimeMillis();
222
223        ByteArrayOutputStream b = new ByteArrayOutputStream(2*1024*1024);
224        try {
225            if (v2) {
226                ViewDebug.dumpv2(rootView, b);
227            } else {
228                ViewDebug.dump(rootView, skipChildren, includeProperties, b);
229            }
230        } catch (IOException | InterruptedException e) {
231            return createFailChunk(1, "Unexpected error while obtaining view hierarchy: "
232                    + e.getMessage());
233        }
234
235        long end = System.currentTimeMillis();
236        Log.d(TAG, "Time to obtain view hierarchy (ms): " + (end - start));
237
238        byte[] data = b.toByteArray();
239        return new Chunk(CHUNK_VURT, data, 0, data.length);
240    }
241
242    /** Returns a buffer with region details & bitmap of every single view. */
243    private Chunk captureLayers(View rootView) {
244        ByteArrayOutputStream b = new ByteArrayOutputStream(1024);
245        DataOutputStream dos = new DataOutputStream(b);
246        try {
247            ViewDebug.captureLayers(rootView, dos);
248        } catch (IOException e) {
249            return createFailChunk(1, "Unexpected error while obtaining view hierarchy: "
250                    + e.getMessage());
251        } finally {
252            try {
253                dos.close();
254            } catch (IOException e) {
255                // ignore
256            }
257        }
258
259        byte[] data = b.toByteArray();
260        return new Chunk(CHUNK_VURT, data, 0, data.length);
261    }
262
263    /**
264     * Returns the Theme dump of the provided view.
265     */
266    private Chunk dumpTheme(View rootView) {
267        ByteArrayOutputStream b = new ByteArrayOutputStream(1024);
268        try {
269            ViewDebug.dumpTheme(rootView, b);
270        } catch (IOException e) {
271            return createFailChunk(1, "Unexpected error while dumping the theme: "
272                    + e.getMessage());
273        }
274
275        byte[] data = b.toByteArray();
276        return new Chunk(CHUNK_VURT, data, 0, data.length);
277    }
278
279    private Chunk captureView(View rootView, View targetView) {
280        ByteArrayOutputStream b = new ByteArrayOutputStream(1024);
281        try {
282            ViewDebug.capture(rootView, b, targetView);
283        } catch (IOException e) {
284            return createFailChunk(1, "Unexpected error while capturing view: "
285                    + e.getMessage());
286        }
287
288        byte[] data = b.toByteArray();
289        return new Chunk(CHUNK_VUOP, data, 0, data.length);
290    }
291
292    /** Returns the display lists corresponding to the provided view. */
293    private Chunk dumpDisplayLists(final View rootView, final View targetView) {
294        rootView.post(new Runnable() {
295            @Override
296            public void run() {
297                ViewDebug.outputDisplayList(rootView, targetView);
298            }
299        });
300        return null;
301    }
302
303    /**
304     * Invokes provided method on the view.
305     * The method name and its arguments are passed in as inputs via the byte buffer.
306     * The buffer contains:<ol>
307     *  <li> len(method name) </li>
308     *  <li> method name </li>
309     *  <li> # of args </li>
310     *  <li> arguments: Each argument comprises of a type specifier followed by the actual argument.
311     *          The type specifier is a single character as used in JNI:
312     *          (Z - boolean, B - byte, C - char, S - short, I - int, J - long,
313     *          F - float, D - double). <p>
314     *          The type specifier is followed by the actual value of argument.
315     *          Booleans are encoded via bytes with 0 indicating false.</li>
316     * </ol>
317     * Methods that take no arguments need only specify the method name.
318     */
319    private Chunk invokeViewMethod(final View rootView, final View targetView, ByteBuffer in) {
320        int l = in.getInt();
321        String methodName = getString(in, l);
322
323        Class<?>[] argTypes;
324        Object[] args;
325        if (!in.hasRemaining()) {
326            argTypes = new Class<?>[0];
327            args = new Object[0];
328        } else {
329            int nArgs = in.getInt();
330
331            argTypes = new Class<?>[nArgs];
332            args = new Object[nArgs];
333
334            for (int i = 0; i < nArgs; i++) {
335                char c = in.getChar();
336                switch (c) {
337                    case 'Z':
338                        argTypes[i] = boolean.class;
339                        args[i] = in.get() == 0 ? false : true;
340                        break;
341                    case 'B':
342                        argTypes[i] = byte.class;
343                        args[i] = in.get();
344                        break;
345                    case 'C':
346                        argTypes[i] = char.class;
347                        args[i] = in.getChar();
348                        break;
349                    case 'S':
350                        argTypes[i] = short.class;
351                        args[i] = in.getShort();
352                        break;
353                    case 'I':
354                        argTypes[i] = int.class;
355                        args[i] = in.getInt();
356                        break;
357                    case 'J':
358                        argTypes[i] = long.class;
359                        args[i] = in.getLong();
360                        break;
361                    case 'F':
362                        argTypes[i] = float.class;
363                        args[i] = in.getFloat();
364                        break;
365                    case 'D':
366                        argTypes[i] = double.class;
367                        args[i] = in.getDouble();
368                        break;
369                    default:
370                        Log.e(TAG, "arg " + i + ", unrecognized type: " + c);
371                        return createFailChunk(ERR_INVALID_PARAM,
372                                "Unsupported parameter type (" + c + ") to invoke view method.");
373                }
374            }
375        }
376
377        Method method = null;
378        try {
379            method = targetView.getClass().getMethod(methodName, argTypes);
380        } catch (NoSuchMethodException e) {
381            Log.e(TAG, "No such method: " + e.getMessage());
382            return createFailChunk(ERR_INVALID_PARAM,
383                    "No such method: " + e.getMessage());
384        }
385
386        try {
387            ViewDebug.invokeViewMethod(targetView, method, args);
388        } catch (Exception e) {
389            Log.e(TAG, "Exception while invoking method: " + e.getCause().getMessage());
390            String msg = e.getCause().getMessage();
391            if (msg == null) {
392                msg = e.getCause().toString();
393            }
394            return createFailChunk(ERR_EXCEPTION, msg);
395        }
396
397        return null;
398    }
399
400    private Chunk setLayoutParameter(final View rootView, final View targetView, ByteBuffer in) {
401        int l = in.getInt();
402        String param = getString(in, l);
403        int value = in.getInt();
404        try {
405            ViewDebug.setLayoutParameter(targetView, param, value);
406        } catch (Exception e) {
407            Log.e(TAG, "Exception setting layout parameter: " + e);
408            return createFailChunk(ERR_EXCEPTION, "Error accessing field "
409                        + param + ":" + e.getMessage());
410        }
411
412        return null;
413    }
414
415    /** Profiles provided view. */
416    private Chunk profileView(View rootView, final View targetView) {
417        ByteArrayOutputStream b = new ByteArrayOutputStream(32 * 1024);
418        BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(b), 32 * 1024);
419        try {
420            ViewDebug.profileViewAndChildren(targetView, bw);
421        } catch (IOException e) {
422            return createFailChunk(1, "Unexpected error while profiling view: " + e.getMessage());
423        } finally {
424            try {
425                bw.close();
426            } catch (IOException e) {
427                // ignore
428            }
429        }
430
431        byte[] data = b.toByteArray();
432        return new Chunk(CHUNK_VUOP, data, 0, data.length);
433    }
434}
435