Tracer.java

/*
 * 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 androidx.test.uiautomator;

import android.util.Log;

import androidx.annotation.NonNull;

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 default 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 = Tracer.class.getPackage().getName();
    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 static class FileSink implements TracerSink {
        private final PrintWriter mOut;
        private final SimpleDateFormat mDateFormat;

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

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

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

    static class LogcatSink implements TracerSink {

        private static final String LOGCAT_TAG = "UiAutomatorTrace";

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

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

    private Mode mCurrentMode = Mode.NONE;
    private final List<TracerSink> 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
     */
    @NonNull
    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(@NonNull 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(@NonNull 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 that's 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 outside 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(@NonNull 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 decide 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());
    }
}