1 /*
2  * Copyright (C) 2012 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 package com.android.uiautomator.core;
17 
18 import android.util.Log;
19 
20 import java.io.File;
21 import java.io.FileNotFoundException;
22 import java.io.PrintWriter;
23 import java.text.SimpleDateFormat;
24 import java.util.ArrayList;
25 import java.util.Arrays;
26 import java.util.Date;
27 import java.util.List;
28 import java.util.Locale;
29 
30 /**
31  * Class that creates traces of the calls to the UiAutomator API and outputs the
32  * traces either to logcat or a logfile. Each public method in the UiAutomator
33  * that needs to be traced should include a call to Tracer.trace in the
34  * beginning. Tracing is turned off by defualt and needs to be enabled
35  * explicitly.
36  * @hide
37  */
38 public class Tracer {
39     private static final String UNKNOWN_METHOD_STRING = "(unknown method)";
40     private static final String UIAUTOMATOR_PACKAGE = "com.android.uiautomator.core";
41     private static final int CALLER_LOCATION = 6;
42     private static final int METHOD_TO_TRACE_LOCATION = 5;
43     private static final int MIN_STACK_TRACE_LENGTH = 7;
44 
45     /**
46      * Enum that determines where the trace output goes. It can go to either
47      * logcat, log file or both.
48      */
49     public enum Mode {
50         NONE,
51         FILE,
52         LOGCAT,
53         ALL
54     }
55 
56     private interface TracerSink {
log(String message)57         public void log(String message);
58 
close()59         public void close();
60     }
61 
62     private class FileSink implements TracerSink {
63         private PrintWriter mOut;
64         private SimpleDateFormat mDateFormat;
65 
FileSink(File file)66         public FileSink(File file) throws FileNotFoundException {
67             mOut = new PrintWriter(file);
68             mDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.US);
69         }
70 
log(String message)71         public void log(String message) {
72             mOut.printf("%s %s\n", mDateFormat.format(new Date()), message);
73         }
74 
close()75         public void close() {
76             mOut.close();
77         }
78     }
79 
80     private class LogcatSink implements TracerSink {
81 
82         private static final String LOGCAT_TAG = "UiAutomatorTrace";
83 
log(String message)84         public void log(String message) {
85             Log.i(LOGCAT_TAG, message);
86         }
87 
close()88         public void close() {
89             // nothing is needed
90         }
91     }
92 
93     private Mode mCurrentMode = Mode.NONE;
94     private List<TracerSink> mSinks = new ArrayList<TracerSink>();
95     private File mOutputFile;
96 
97     private static Tracer mInstance = null;
98 
99     /**
100      * Returns a reference to an instance of the tracer. Useful to set the
101      * parameters before the trace is collected.
102      *
103      * @return
104      */
getInstance()105     public static Tracer getInstance() {
106         if (mInstance == null) {
107             mInstance = new Tracer();
108         }
109         return mInstance;
110     }
111 
112     /**
113      * Sets where the trace output will go. Can be either be logcat or a file or
114      * both. Setting this to NONE will turn off tracing.
115      *
116      * @param mode
117      */
setOutputMode(Mode mode)118     public void setOutputMode(Mode mode) {
119         closeSinks();
120         mCurrentMode = mode;
121         try {
122             switch (mode) {
123                 case FILE:
124                     if (mOutputFile == null) {
125                         throw new IllegalArgumentException("Please provide a filename before " +
126                                 "attempting write trace to a file");
127                     }
128                     mSinks.add(new FileSink(mOutputFile));
129                     break;
130                 case LOGCAT:
131                     mSinks.add(new LogcatSink());
132                     break;
133                 case ALL:
134                     mSinks.add(new LogcatSink());
135                     if (mOutputFile == null) {
136                         throw new IllegalArgumentException("Please provide a filename before " +
137                                 "attempting write trace to a file");
138                     }
139                     mSinks.add(new FileSink(mOutputFile));
140                     break;
141                 default:
142                     break;
143             }
144         } catch (FileNotFoundException e) {
145             Log.w("Tracer", "Could not open log file: " + e.getMessage());
146         }
147     }
148 
closeSinks()149     private void closeSinks() {
150         for (TracerSink sink : mSinks) {
151             sink.close();
152         }
153         mSinks.clear();
154     }
155 
156     /**
157      * Sets the name of the log file where tracing output will be written if the
158      * tracer is set to write to a file.
159      *
160      * @param filename name of the log file.
161      */
setOutputFilename(String filename)162     public void setOutputFilename(String filename) {
163         mOutputFile = new File(filename);
164     }
165 
doTrace(Object[] arguments)166     private void doTrace(Object[] arguments) {
167         if (mCurrentMode == Mode.NONE) {
168             return;
169         }
170 
171         String caller = getCaller();
172         if (caller == null) {
173             return;
174         }
175 
176         log(String.format("%s (%s)", caller, join(", ", arguments)));
177     }
178 
log(String message)179     private void log(String message) {
180         for (TracerSink sink : mSinks) {
181             sink.log(message);
182         }
183     }
184 
185     /**
186      * Queries whether the tracing is enabled.
187      * @return true if tracing is enabled, false otherwise.
188      */
isTracingEnabled()189     public boolean isTracingEnabled() {
190         return mCurrentMode != Mode.NONE;
191     }
192 
193     /**
194      * Public methods in the UiAutomator should call this function to generate a
195      * trace. The trace will include the method thats is being called, it's
196      * arguments and where in the user's code the method is called from. If a
197      * public method is called internally from UIAutomator then this will not
198      * output a trace entry. Only calls from outise the UiAutomator package will
199      * produce output.
200      *
201      * Special note about array arguments. You can safely pass arrays of reference types
202      * to this function. Like String[] or Integer[]. The trace function will print their
203      * contents by calling toString() on each of the elements. This will not work for
204      * array of primitive types like int[] or float[]. Before passing them to this function
205      * convert them to arrays of reference types manually. Example: convert int[] to Integer[].
206      *
207      * @param arguments arguments of the method being traced.
208      */
trace(Object... arguments)209     public static void trace(Object... arguments) {
210         Tracer.getInstance().doTrace(arguments);
211     }
212 
join(String separator, Object[] strings)213     private static String join(String separator, Object[] strings) {
214         if (strings.length == 0)
215             return "";
216 
217         StringBuilder builder = new StringBuilder(objectToString(strings[0]));
218         for (int i = 1; i < strings.length; i++) {
219             builder.append(separator);
220             builder.append(objectToString(strings[i]));
221         }
222         return builder.toString();
223     }
224 
225     /**
226      * Special toString method to handle arrays. If the argument is a normal object then this will
227      * return normal output of obj.toString(). If the argument is an array this will return a
228      * string representation of the elements of the array.
229      *
230      * This method will not work for arrays of primitive types. Arrays of primitive types are
231      * expected to be converted manually by the caller. If the array is not converter then
232      * this function will only output "[...]" instead of the contents of the array.
233      *
234      * @param obj object to convert to a string
235      * @return String representation of the object.
236      */
objectToString(Object obj)237     private static String objectToString(Object obj) {
238         if (obj.getClass().isArray()) {
239             if (obj instanceof Object[]) {
240                 return Arrays.deepToString((Object[])obj);
241             } else {
242                 return "[...]";
243             }
244         } else {
245             return obj.toString();
246         }
247     }
248 
249     /**
250      * This method outputs which UiAutomator method was called and where in the
251      * user code it was called from. If it can't deside which method is called
252      * it will output "(unknown method)". If the method was called from inside
253      * the UiAutomator then it returns null.
254      *
255      * @return name of the method called and where it was called from. Null if
256      *         method was called from inside UiAutomator.
257      */
getCaller()258     private static String getCaller() {
259         StackTraceElement stackTrace[] = Thread.currentThread().getStackTrace();
260         if (stackTrace.length < MIN_STACK_TRACE_LENGTH) {
261             return UNKNOWN_METHOD_STRING;
262         }
263 
264         StackTraceElement caller = stackTrace[METHOD_TO_TRACE_LOCATION];
265         StackTraceElement previousCaller = stackTrace[CALLER_LOCATION];
266 
267         if (previousCaller.getClassName().startsWith(UIAUTOMATOR_PACKAGE)) {
268             return null;
269         }
270 
271         int indexOfDot = caller.getClassName().lastIndexOf('.');
272         if (indexOfDot < 0) {
273             indexOfDot = 0;
274         }
275 
276         if (indexOfDot + 1 >= caller.getClassName().length()) {
277             return UNKNOWN_METHOD_STRING;
278         }
279 
280         String shortClassName = caller.getClassName().substring(indexOfDot + 1);
281         return String.format("%s.%s from %s() at %s:%d", shortClassName, caller.getMethodName(),
282                 previousCaller.getMethodName(), previousCaller.getFileName(),
283                 previousCaller.getLineNumber());
284     }
285 }
286