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 
17 package android.ddm;
18 
19 import android.util.Log;
20 import android.view.View;
21 import android.view.ViewDebug;
22 import android.view.ViewRootImpl;
23 import android.view.WindowManagerGlobal;
24 
25 import org.apache.harmony.dalvik.ddmc.Chunk;
26 import org.apache.harmony.dalvik.ddmc.ChunkHandler;
27 import org.apache.harmony.dalvik.ddmc.DdmServer;
28 
29 import java.io.BufferedWriter;
30 import java.io.ByteArrayOutputStream;
31 import java.io.DataOutputStream;
32 import java.io.IOException;
33 import java.io.OutputStreamWriter;
34 import java.lang.reflect.Method;
35 import java.nio.BufferUnderflowException;
36 import 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  */
42 public 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. */
DdmHandleViewDebug()93     private DdmHandleViewDebug() {}
94 
register()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
connected()102     public void connected() {
103     }
104 
105     @Override
disconnected()106     public void disconnected() {
107     }
108 
109     @Override
handleChunk(Chunk request)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. */
listWindows()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 
getRootView(ByteBuffer in)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 
getTargetView(View root, ByteBuffer in)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      */
dumpHierarchy(View rootView, ByteBuffer in)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. */
captureLayers(View rootView)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      */
dumpTheme(View rootView)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 
captureView(View rootView, View targetView)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. */
dumpDisplayLists(final View rootView, final View targetView)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      */
invokeViewMethod(final View rootView, final View targetView, ByteBuffer in)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 
setLayoutParameter(final View rootView, final View targetView, ByteBuffer in)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. */
profileView(View rootView, final View targetView)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