/* * 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 mSinks = new ArrayList(); 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()); } }