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 */
16package com.android.uiautomator.core;
17
18import android.util.Log;
19
20import java.io.File;
21import java.io.FileNotFoundException;
22import java.io.PrintWriter;
23import java.text.SimpleDateFormat;
24import java.util.ArrayList;
25import java.util.Arrays;
26import java.util.Date;
27import java.util.List;
28import 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 */
38public 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 {
57        public void log(String message);
58
59        public void close();
60    }
61
62    private class FileSink implements TracerSink {
63        private PrintWriter mOut;
64        private SimpleDateFormat mDateFormat;
65
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
71        public void log(String message) {
72            mOut.printf("%s %s\n", mDateFormat.format(new Date()), message);
73        }
74
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
84        public void log(String message) {
85            Log.i(LOGCAT_TAG, message);
86        }
87
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     */
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     */
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
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     */
162    public void setOutputFilename(String filename) {
163        mOutputFile = new File(filename);
164    }
165
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
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     */
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     */
209    public static void trace(Object... arguments) {
210        Tracer.getInstance().doTrace(arguments);
211    }
212
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     */
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     */
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