summaryrefslogtreecommitdiff
path: root/src/com/android/uiautomator/core/Tracer.java
blob: d574fc064f7facc68cdf446ef2dfaabeae7c9831 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
/*
 * Copyright (C) 2012 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.android.uiautomator.core;

import android.util.Log;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.PrintWriter;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.Locale;

/**
 * Class that creates traces of the calls to the UiAutomator API and outputs the
 * traces either to logcat or a logfile. Each public method in the UiAutomator
 * that needs to be traced should include a call to Tracer.trace in the
 * beginning. Tracing is turned off by defualt and needs to be enabled
 * explicitly.
 * @hide
 */
public class Tracer {
    private static final String UNKNOWN_METHOD_STRING = "(unknown method)";
    private static final String UIAUTOMATOR_PACKAGE = "com.android.uiautomator.core";
    private static final int CALLER_LOCATION = 6;
    private static final int METHOD_TO_TRACE_LOCATION = 5;
    private static final int MIN_STACK_TRACE_LENGTH = 7;

    /**
     * Enum that determines where the trace output goes. It can go to either
     * logcat, log file or both.
     */
    public enum Mode {
        NONE,
        FILE,
        LOGCAT,
        ALL
    }

    private interface TracerSink {
        public void log(String message);

        public void close();
    }

    private class FileSink implements TracerSink {
        private PrintWriter mOut;
        private SimpleDateFormat mDateFormat;

        public FileSink(File file) throws FileNotFoundException {
            mOut = new PrintWriter(file);
            mDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.US);
        }

        public void log(String message) {
            mOut.printf("%s %s\n", mDateFormat.format(new Date()), message);
        }

        public void close() {
            mOut.close();
        }
    }

    private class LogcatSink implements TracerSink {

        private static final String LOGCAT_TAG = "UiAutomatorTrace";

        public void log(String message) {
            Log.i(LOGCAT_TAG, message);
        }

        public void close() {
            // nothing is needed
        }
    }

    private Mode mCurrentMode = Mode.NONE;
    private List<TracerSink> mSinks = new ArrayList<TracerSink>();
    private File mOutputFile;

    private static Tracer mInstance = null;

    /**
     * Returns a reference to an instance of the tracer. Useful to set the
     * parameters before the trace is collected.
     *
     * @return
     */
    public static Tracer getInstance() {
        if (mInstance == null) {
            mInstance = new Tracer();
        }
        return mInstance;
    }

    /**
     * Sets where the trace output will go. Can be either be logcat or a file or
     * both. Setting this to NONE will turn off tracing.
     *
     * @param mode
     */
    public void setOutputMode(Mode mode) {
        closeSinks();
        mCurrentMode = mode;
        try {
            switch (mode) {
                case FILE:
                    if (mOutputFile == null) {
                        throw new IllegalArgumentException("Please provide a filename before " +
                                "attempting write trace to a file");
                    }
                    mSinks.add(new FileSink(mOutputFile));
                    break;
                case LOGCAT:
                    mSinks.add(new LogcatSink());
                    break;
                case ALL:
                    mSinks.add(new LogcatSink());
                    if (mOutputFile == null) {
                        throw new IllegalArgumentException("Please provide a filename before " +
                                "attempting write trace to a file");
                    }
                    mSinks.add(new FileSink(mOutputFile));
                    break;
                default:
                    break;
            }
        } catch (FileNotFoundException e) {
            Log.w("Tracer", "Could not open log file: " + e.getMessage());
        }
    }

    private void closeSinks() {
        for (TracerSink sink : mSinks) {
            sink.close();
        }
        mSinks.clear();
    }

    /**
     * Sets the name of the log file where tracing output will be written if the
     * tracer is set to write to a file.
     *
     * @param filename name of the log file.
     */
    public void setOutputFilename(String filename) {
        mOutputFile = new File(filename);
    }

    private void doTrace(Object[] arguments) {
        if (mCurrentMode == Mode.NONE) {
            return;
        }

        String caller = getCaller();
        if (caller == null) {
            return;
        }

        log(String.format("%s (%s)", caller, join(", ", arguments)));
    }

    private void log(String message) {
        for (TracerSink sink : mSinks) {
            sink.log(message);
        }
    }

    /**
     * Queries whether the tracing is enabled.
     * @return true if tracing is enabled, false otherwise.
     */
    public boolean isTracingEnabled() {
        return mCurrentMode != Mode.NONE;
    }

    /**
     * Public methods in the UiAutomator should call this function to generate a
     * trace. The trace will include the method thats is being called, it's
     * arguments and where in the user's code the method is called from. If a
     * public method is called internally from UIAutomator then this will not
     * output a trace entry. Only calls from outise the UiAutomator package will
     * produce output.
     *
     * Special note about array arguments. You can safely pass arrays of reference types
     * to this function. Like String[] or Integer[]. The trace function will print their
     * contents by calling toString() on each of the elements. This will not work for
     * array of primitive types like int[] or float[]. Before passing them to this function
     * convert them to arrays of reference types manually. Example: convert int[] to Integer[].
     *
     * @param arguments arguments of the method being traced.
     */
    public static void trace(Object... arguments) {
        Tracer.getInstance().doTrace(arguments);
    }

    private static String join(String separator, Object[] strings) {
        if (strings.length == 0)
            return "";

        StringBuilder builder = new StringBuilder(objectToString(strings[0]));
        for (int i = 1; i < strings.length; i++) {
            builder.append(separator);
            builder.append(objectToString(strings[i]));
        }
        return builder.toString();
    }

    /**
     * Special toString method to handle arrays. If the argument is a normal object then this will
     * return normal output of obj.toString(). If the argument is an array this will return a
     * string representation of the elements of the array.
     *
     * This method will not work for arrays of primitive types. Arrays of primitive types are
     * expected to be converted manually by the caller. If the array is not converter then
     * this function will only output "[...]" instead of the contents of the array.
     *
     * @param obj object to convert to a string
     * @return String representation of the object.
     */
    private static String objectToString(Object obj) {
        if (obj.getClass().isArray()) {
            if (obj instanceof Object[]) {
                return Arrays.deepToString((Object[])obj);
            } else {
                return "[...]";
            }
        } else {
            return obj.toString();
        }
    }

    /**
     * This method outputs which UiAutomator method was called and where in the
     * user code it was called from. If it can't deside which method is called
     * it will output "(unknown method)". If the method was called from inside
     * the UiAutomator then it returns null.
     *
     * @return name of the method called and where it was called from. Null if
     *         method was called from inside UiAutomator.
     */
    private static String getCaller() {
        StackTraceElement stackTrace[] = Thread.currentThread().getStackTrace();
        if (stackTrace.length < MIN_STACK_TRACE_LENGTH) {
            return UNKNOWN_METHOD_STRING;
        }

        StackTraceElement caller = stackTrace[METHOD_TO_TRACE_LOCATION];
        StackTraceElement previousCaller = stackTrace[CALLER_LOCATION];

        if (previousCaller.getClassName().startsWith(UIAUTOMATOR_PACKAGE)) {
            return null;
        }

        int indexOfDot = caller.getClassName().lastIndexOf('.');
        if (indexOfDot < 0) {
            indexOfDot = 0;
        }

        if (indexOfDot + 1 >= caller.getClassName().length()) {
            return UNKNOWN_METHOD_STRING;
        }

        String shortClassName = caller.getClassName().substring(indexOfDot + 1);
        return String.format("%s.%s from %s() at %s:%d", shortClassName, caller.getMethodName(),
                previousCaller.getMethodName(), previousCaller.getFileName(),
                previousCaller.getLineNumber());
    }
}