logAdapters = new ArrayList<>();
+
+ @Override public Printer t(String tag) {
+ if (tag != null) {
+ localTag.set(tag);
+ }
+ return this;
+ }
+
+ @Override public void d(@NonNull String message, @Nullable Object... args) {
+ log(DEBUG, null, message, args);
+ }
+
+ @Override public void d(@Nullable Object object) {
+ log(DEBUG, null, Utils.toString(object));
+ }
+
+ @Override public void e(@NonNull String message, @Nullable Object... args) {
+ e(null, message, args);
+ }
+
+ @Override public void e(@Nullable Throwable throwable, @NonNull String message, @Nullable Object... args) {
+ log(ERROR, throwable, message, args);
+ }
+
+ @Override public void w(@NonNull String message, @Nullable Object... args) {
+ log(WARN, null, message, args);
+ }
+
+ @Override public void i(@NonNull String message, @Nullable Object... args) {
+ log(INFO, null, message, args);
+ }
+
+ @Override public void v(@NonNull String message, @Nullable Object... args) {
+ log(VERBOSE, null, message, args);
+ }
+
+ @Override public void wtf(@NonNull String message, @Nullable Object... args) {
+ log(ASSERT, null, message, args);
+ }
+
+ @Override public void json(@Nullable String json) {
+ if (Utils.isEmpty(json)) {
+ d("Empty/Null json content");
+ return;
+ }
+ try {
+ json = json.trim();
+ if (json.startsWith("{")) {
+ JSONObject jsonObject = new JSONObject(json);
+ String message = jsonObject.toString(JSON_INDENT);
+ d(message);
+ return;
+ }
+ if (json.startsWith("[")) {
+ JSONArray jsonArray = new JSONArray(json);
+ String message = jsonArray.toString(JSON_INDENT);
+ d(message);
+ return;
+ }
+ e("Invalid Json");
+ } catch (JSONException e) {
+ e("Invalid Json");
+ }
+ }
+
+ @Override public void xml(@Nullable String xml) {
+ if (Utils.isEmpty(xml)) {
+ d("Empty/Null xml content");
+ return;
+ }
+ try {
+ Source xmlInput = new StreamSource(new StringReader(xml));
+ StreamResult xmlOutput = new StreamResult(new StringWriter());
+ Transformer transformer = TransformerFactory.newInstance().newTransformer();
+ transformer.setOutputProperty(OutputKeys.INDENT, "yes");
+ transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2");
+ transformer.transform(xmlInput, xmlOutput);
+ d(xmlOutput.getWriter().toString().replaceFirst(">", ">\n"));
+ } catch (TransformerException e) {
+ e("Invalid xml");
+ }
+ }
+
+ @Override public synchronized void log(int priority,
+ @Nullable String tag,
+ @Nullable String message,
+ @Nullable Throwable throwable) {
+ if (throwable != null && message != null) {
+ message += " : " + Utils.getStackTraceString(throwable);
+ }
+ if (throwable != null && message == null) {
+ message = Utils.getStackTraceString(throwable);
+ }
+ if (Utils.isEmpty(message)) {
+ message = "Empty/NULL log message";
+ }
+
+ for (LogAdapter adapter : logAdapters) {
+ if (adapter.isLoggable(priority, tag)) {
+ adapter.log(priority, tag, message);
+ }
+ }
+ }
+
+ @Override public void clearLogAdapters() {
+ logAdapters.clear();
+ }
+
+ @Override public void addAdapter(@NonNull LogAdapter adapter) {
+ logAdapters.add(checkNotNull(adapter));
+ }
+
+ /**
+ * This method is synchronized in order to avoid messy of logs' order.
+ */
+ private synchronized void log(int priority,
+ @Nullable Throwable throwable,
+ @NonNull String msg,
+ @Nullable Object... args) {
+ checkNotNull(msg);
+
+ String tag = getTag();
+ String message = createMessage(msg, args);
+ log(priority, tag, message, throwable);
+ }
+
+ /**
+ * @return the appropriate tag based on local or global
+ */
+ @Nullable private String getTag() {
+ String tag = localTag.get();
+ if (tag != null) {
+ localTag.remove();
+ return tag;
+ }
+ return null;
+ }
+
+ @NonNull private String createMessage(@NonNull String message, @Nullable Object... args) {
+ return args == null || args.length == 0 ? message : String.format(message, args);
+ }
+}
diff --git a/QYlogger/src/main/java/com/orhanobut/logger/PrettyFormatStrategy.java b/QYlogger/src/main/java/com/orhanobut/logger/PrettyFormatStrategy.java
new file mode 100644
index 0000000..74e33ad
--- /dev/null
+++ b/QYlogger/src/main/java/com/orhanobut/logger/PrettyFormatStrategy.java
@@ -0,0 +1,256 @@
+package com.orhanobut.logger;
+
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+
+import static com.orhanobut.logger.Utils.checkNotNull;
+
+/**
+ * Draws borders around the given log message along with additional information such as :
+ *
+ *
+ * - Thread information
+ * - Method stack trace
+ *
+ *
+ *
+ * ┌──────────────────────────
+ * │ Method stack history
+ * ├┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
+ * │ Thread information
+ * ├┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
+ * │ Log message
+ * └──────────────────────────
+ *
+ *
+ * Customize
+ *
+ * FormatStrategy formatStrategy = PrettyFormatStrategy.newBuilder()
+ * .showThreadInfo(false) // (Optional) Whether to show thread info or not. Default true
+ * .methodCount(0) // (Optional) How many method line to show. Default 2
+ * .methodOffset(7) // (Optional) Hides internal method calls up to offset. Default 5
+ * .logStrategy(customLog) // (Optional) Changes the log strategy to print out. Default LogCat
+ * .tag("My custom tag") // (Optional) Global tag for every log. Default PRETTY_LOGGER
+ * .build();
+ *
+ */
+public class PrettyFormatStrategy implements FormatStrategy {
+
+ /**
+ * Android's max limit for a log entry is ~4076 bytes,
+ * so 4000 bytes is used as chunk size since default charset
+ * is UTF-8
+ */
+ private static final int CHUNK_SIZE = 4000;
+
+ /**
+ * The minimum stack trace index, starts at this class after two native calls.
+ */
+ private static final int MIN_STACK_OFFSET = 5;
+
+ /**
+ * Drawing toolbox
+ */
+ private static final char TOP_LEFT_CORNER = '┌';
+ private static final char BOTTOM_LEFT_CORNER = '└';
+ private static final char MIDDLE_CORNER = '├';
+ private static final char HORIZONTAL_LINE = '│';
+ private static final String DOUBLE_DIVIDER = "────────────────────────────────────────────────────────";
+ private static final String SINGLE_DIVIDER = "┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄";
+ private static final String TOP_BORDER = TOP_LEFT_CORNER + DOUBLE_DIVIDER + DOUBLE_DIVIDER;
+ private static final String BOTTOM_BORDER = BOTTOM_LEFT_CORNER + DOUBLE_DIVIDER + DOUBLE_DIVIDER;
+ private static final String MIDDLE_BORDER = MIDDLE_CORNER + SINGLE_DIVIDER + SINGLE_DIVIDER;
+
+ private final int methodCount;
+ private final int methodOffset;
+ private final boolean showThreadInfo;
+ @NonNull private final LogStrategy logStrategy;
+ @Nullable private final String tag;
+
+ private PrettyFormatStrategy(@NonNull Builder builder) {
+ checkNotNull(builder);
+
+ methodCount = builder.methodCount;
+ methodOffset = builder.methodOffset;
+ showThreadInfo = builder.showThreadInfo;
+ logStrategy = builder.logStrategy;
+ tag = builder.tag;
+ }
+
+ @NonNull public static Builder newBuilder() {
+ return new Builder();
+ }
+
+ @Override public void log(int priority, @Nullable String onceOnlyTag, @NonNull String message) {
+ checkNotNull(message);
+
+ String tag = formatTag(onceOnlyTag);
+
+ logTopBorder(priority, tag);
+ logHeaderContent(priority, tag, methodCount);
+
+ //get bytes of message with system's default charset (which is UTF-8 for Android)
+ byte[] bytes = message.getBytes();
+ int length = bytes.length;
+ if (length <= CHUNK_SIZE) {
+ if (methodCount > 0) {
+ logDivider(priority, tag);
+ }
+ logContent(priority, tag, message);
+ logBottomBorder(priority, tag);
+ return;
+ }
+ if (methodCount > 0) {
+ logDivider(priority, tag);
+ }
+ for (int i = 0; i < length; i += CHUNK_SIZE) {
+ int count = Math.min(length - i, CHUNK_SIZE);
+ //create a new String with system's default charset (which is UTF-8 for Android)
+ logContent(priority, tag, new String(bytes, i, count));
+ }
+ logBottomBorder(priority, tag);
+ }
+
+ private void logTopBorder(int logType, @Nullable String tag) {
+ logChunk(logType, tag, TOP_BORDER);
+ }
+
+ @SuppressWarnings("StringBufferReplaceableByString")
+ private void logHeaderContent(int logType, @Nullable String tag, int methodCount) {
+ StackTraceElement[] trace = Thread.currentThread().getStackTrace();
+ if (showThreadInfo) {
+ logChunk(logType, tag, HORIZONTAL_LINE + " Thread: " + Thread.currentThread().getName());
+ logDivider(logType, tag);
+ }
+ String level = "";
+
+ int stackOffset = getStackOffset(trace) + methodOffset;
+
+ //corresponding method count with the current stack may exceeds the stack trace. Trims the count
+ if (methodCount + stackOffset > trace.length) {
+ methodCount = trace.length - stackOffset - 1;
+ }
+
+ for (int i = methodCount; i > 0; i--) {
+ int stackIndex = i + stackOffset;
+ if (stackIndex >= trace.length) {
+ continue;
+ }
+ StringBuilder builder = new StringBuilder();
+ builder.append(HORIZONTAL_LINE)
+ .append(' ')
+ .append(level)
+ .append(getSimpleClassName(trace[stackIndex].getClassName()))
+ .append(".")
+ .append(trace[stackIndex].getMethodName())
+ .append(" ")
+ .append(" (")
+ .append(trace[stackIndex].getFileName())
+ .append(":")
+ .append(trace[stackIndex].getLineNumber())
+ .append(")");
+ level += " ";
+ logChunk(logType, tag, builder.toString());
+ }
+ }
+
+ private void logBottomBorder(int logType, @Nullable String tag) {
+ logChunk(logType, tag, BOTTOM_BORDER);
+ }
+
+ private void logDivider(int logType, @Nullable String tag) {
+ logChunk(logType, tag, MIDDLE_BORDER);
+ }
+
+ private void logContent(int logType, @Nullable String tag, @NonNull String chunk) {
+ checkNotNull(chunk);
+
+ String[] lines = chunk.split(System.getProperty("line.separator"));
+ for (String line : lines) {
+ logChunk(logType, tag, HORIZONTAL_LINE + " " + line);
+ }
+ }
+
+ private void logChunk(int priority, @Nullable String tag, @NonNull String chunk) {
+ checkNotNull(chunk);
+
+ logStrategy.log(priority, tag, chunk);
+ }
+
+ private String getSimpleClassName(@NonNull String name) {
+ checkNotNull(name);
+
+ int lastIndex = name.lastIndexOf(".");
+ return name.substring(lastIndex + 1);
+ }
+
+ /**
+ * Determines the starting index of the stack trace, after method calls made by this class.
+ *
+ * @param trace the stack trace
+ * @return the stack offset
+ */
+ private int getStackOffset(@NonNull StackTraceElement[] trace) {
+ checkNotNull(trace);
+
+ for (int i = MIN_STACK_OFFSET; i < trace.length; i++) {
+ StackTraceElement e = trace[i];
+ String name = e.getClassName();
+ if (!name.equals(LoggerPrinter.class.getName()) && !name.equals(Logger.class.getName())) {
+ return --i;
+ }
+ }
+ return -1;
+ }
+
+ @Nullable private String formatTag(@Nullable String tag) {
+ if (!Utils.isEmpty(tag) && !Utils.equals(this.tag, tag)) {
+ return this.tag + "-" + tag;
+ }
+ return this.tag;
+ }
+
+ public static class Builder {
+ int methodCount = 2;
+ int methodOffset = 0;
+ boolean showThreadInfo = true;
+ @Nullable LogStrategy logStrategy;
+ @Nullable String tag = "PRETTY_LOGGER";
+
+ private Builder() {
+ }
+
+ @NonNull public Builder methodCount(int val) {
+ methodCount = val;
+ return this;
+ }
+
+ @NonNull public Builder methodOffset(int val) {
+ methodOffset = val;
+ return this;
+ }
+
+ @NonNull public Builder showThreadInfo(boolean val) {
+ showThreadInfo = val;
+ return this;
+ }
+
+ @NonNull public Builder logStrategy(@Nullable LogStrategy val) {
+ logStrategy = val;
+ return this;
+ }
+
+ @NonNull public Builder tag(@Nullable String tag) {
+ this.tag = tag;
+ return this;
+ }
+
+ @NonNull public PrettyFormatStrategy build() {
+ if (logStrategy == null) {
+ logStrategy = new LogcatLogStrategy();
+ }
+ return new PrettyFormatStrategy(this);
+ }
+ }
+
+}
diff --git a/QYlogger/src/main/java/com/orhanobut/logger/Printer.java b/QYlogger/src/main/java/com/orhanobut/logger/Printer.java
new file mode 100644
index 0000000..9fad8de
--- /dev/null
+++ b/QYlogger/src/main/java/com/orhanobut/logger/Printer.java
@@ -0,0 +1,45 @@
+package com.orhanobut.logger;
+
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+
+/**
+ * A proxy interface to enable additional operations.
+ * Contains all possible Log message usages.
+ */
+public interface Printer {
+
+ void addAdapter(@NonNull LogAdapter adapter);
+
+ Printer t(@Nullable String tag);
+
+ void d(@NonNull String message, @Nullable Object... args);
+
+ void d(@Nullable Object object);
+
+ void e(@NonNull String message, @Nullable Object... args);
+
+ void e(@Nullable Throwable throwable, @NonNull String message, @Nullable Object... args);
+
+ void w(@NonNull String message, @Nullable Object... args);
+
+ void i(@NonNull String message, @Nullable Object... args);
+
+ void v(@NonNull String message, @Nullable Object... args);
+
+ void wtf(@NonNull String message, @Nullable Object... args);
+
+ /**
+ * Formats the given json content and print it
+ */
+ void json(@Nullable String json);
+
+ /**
+ * Formats the given xml content and print it
+ */
+ void xml(@Nullable String xml);
+
+ void log(int priority, @Nullable String tag, @Nullable String message, @Nullable Throwable throwable);
+
+ void clearLogAdapters();
+}
diff --git a/QYlogger/src/main/java/com/orhanobut/logger/Utils.java b/QYlogger/src/main/java/com/orhanobut/logger/Utils.java
new file mode 100644
index 0000000..1789de2
--- /dev/null
+++ b/QYlogger/src/main/java/com/orhanobut/logger/Utils.java
@@ -0,0 +1,157 @@
+package com.orhanobut.logger;
+
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.net.UnknownHostException;
+import java.util.Arrays;
+
+import static com.orhanobut.logger.Logger.ASSERT;
+import static com.orhanobut.logger.Logger.DEBUG;
+import static com.orhanobut.logger.Logger.ERROR;
+import static com.orhanobut.logger.Logger.INFO;
+import static com.orhanobut.logger.Logger.VERBOSE;
+import static com.orhanobut.logger.Logger.WARN;
+
+/**
+ * Provides convenient methods to some common operations
+ */
+final class Utils {
+
+ private Utils() {
+ // Hidden constructor.
+ }
+
+ /**
+ * Returns true if the string is null or 0-length.
+ *
+ * @param str the string to be examined
+ * @return true if str is null or zero length
+ */
+ static boolean isEmpty(CharSequence str) {
+ return str == null || str.length() == 0;
+ }
+
+ /**
+ * Returns true if a and b are equal, including if they are both null.
+ * Note: In platform versions 1.1 and earlier, this method only worked well if
+ * both the arguments were instances of String.
+ *
+ * @param a first CharSequence to check
+ * @param b second CharSequence to check
+ * @return true if a and b are equal
+ *
+ * NOTE: Logic slightly change due to strict policy on CI -
+ * "Inner assignments should be avoided"
+ */
+ static boolean equals(CharSequence a, CharSequence b) {
+ if (a == b) return true;
+ if (a != null && b != null) {
+ int length = a.length();
+ if (length == b.length()) {
+ if (a instanceof String && b instanceof String) {
+ return a.equals(b);
+ } else {
+ for (int i = 0; i < length; i++) {
+ if (a.charAt(i) != b.charAt(i)) return false;
+ }
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Copied from "android.util.Log.getStackTraceString()" in order to avoid usage of Android stack
+ * in unit tests.
+ *
+ * @return Stack trace in form of String
+ */
+ static String getStackTraceString(Throwable tr) {
+ if (tr == null) {
+ return "";
+ }
+
+ // This is to reduce the amount of log spew that apps do in the non-error
+ // condition of the network being unavailable.
+ Throwable t = tr;
+ while (t != null) {
+ if (t instanceof UnknownHostException) {
+ return "";
+ }
+ t = t.getCause();
+ }
+
+ StringWriter sw = new StringWriter();
+ PrintWriter pw = new PrintWriter(sw);
+ tr.printStackTrace(pw);
+ pw.flush();
+ return sw.toString();
+ }
+
+ static String logLevel(int value) {
+ switch (value) {
+ case VERBOSE:
+ return "VERBOSE";
+ case DEBUG:
+ return "DEBUG";
+ case INFO:
+ return "INFO";
+ case WARN:
+ return "WARN";
+ case ERROR:
+ return "ERROR";
+ case ASSERT:
+ return "ASSERT";
+ default:
+ return "UNKNOWN";
+ }
+ }
+
+ public static String toString(Object object) {
+ if (object == null) {
+ return "null";
+ }
+ if (!object.getClass().isArray()) {
+ return object.toString();
+ }
+ if (object instanceof boolean[]) {
+ return Arrays.toString((boolean[]) object);
+ }
+ if (object instanceof byte[]) {
+ return Arrays.toString((byte[]) object);
+ }
+ if (object instanceof char[]) {
+ return Arrays.toString((char[]) object);
+ }
+ if (object instanceof short[]) {
+ return Arrays.toString((short[]) object);
+ }
+ if (object instanceof int[]) {
+ return Arrays.toString((int[]) object);
+ }
+ if (object instanceof long[]) {
+ return Arrays.toString((long[]) object);
+ }
+ if (object instanceof float[]) {
+ return Arrays.toString((float[]) object);
+ }
+ if (object instanceof double[]) {
+ return Arrays.toString((double[]) object);
+ }
+ if (object instanceof Object[]) {
+ return Arrays.deepToString((Object[]) object);
+ }
+ return "Couldn't find a correct type for the object";
+ }
+
+ @NonNull static T checkNotNull(@Nullable final T obj) {
+ if (obj == null) {
+ throw new NullPointerException();
+ }
+ return obj;
+ }
+}
diff --git a/QYlogger/src/test/java/com.orhanobut.logger/AndroidLogAdapterTest.kt b/QYlogger/src/test/java/com.orhanobut.logger/AndroidLogAdapterTest.kt
new file mode 100644
index 0000000..30851a5
--- /dev/null
+++ b/QYlogger/src/test/java/com.orhanobut.logger/AndroidLogAdapterTest.kt
@@ -0,0 +1,27 @@
+package com.orhanobut.logger
+
+import org.junit.Test
+
+import com.google.common.truth.Truth.assertThat
+import com.orhanobut.logger.Logger.DEBUG
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.verify
+
+class AndroidLogAdapterTest {
+
+ @Test fun isLoggable() {
+ val logAdapter = AndroidLogAdapter()
+
+ assertThat(logAdapter.isLoggable(DEBUG, "tag")).isTrue()
+ }
+
+ @Test fun log() {
+ val formatStrategy = mock(FormatStrategy::class.java)
+ val logAdapter = AndroidLogAdapter(formatStrategy)
+
+ logAdapter.log(DEBUG, null, "message")
+
+ verify(formatStrategy).log(DEBUG, null, "message")
+ }
+
+}
\ No newline at end of file
diff --git a/QYlogger/src/test/java/com.orhanobut.logger/CsvFormatStrategyTest.kt b/QYlogger/src/test/java/com.orhanobut.logger/CsvFormatStrategyTest.kt
new file mode 100644
index 0000000..0f26e52
--- /dev/null
+++ b/QYlogger/src/test/java/com.orhanobut.logger/CsvFormatStrategyTest.kt
@@ -0,0 +1,37 @@
+package com.orhanobut.logger
+
+import org.junit.Test
+
+import com.google.common.truth.Truth.assertThat
+
+class CsvFormatStrategyTest {
+
+ @Test fun log() {
+ val formatStrategy = CsvFormatStrategy.newBuilder()
+ .logStrategy { priority, tag, message ->
+ assertThat(tag).isEqualTo("PRETTY_LOGGER-tag")
+ assertThat(priority).isEqualTo(Logger.VERBOSE)
+ assertThat(message).contains("VERBOSE,PRETTY_LOGGER-tag,message")
+ }
+ .build()
+
+ formatStrategy.log(Logger.VERBOSE, "tag", "message")
+ }
+
+ @Test fun defaultTag() {
+ val formatStrategy = CsvFormatStrategy.newBuilder()
+ .logStrategy { priority, tag, message -> assertThat(tag).isEqualTo("PRETTY_LOGGER") }
+ .build()
+
+ formatStrategy.log(Logger.VERBOSE, null, "message")
+ }
+
+ @Test fun customTag() {
+ val formatStrategy = CsvFormatStrategy.newBuilder()
+ .tag("custom")
+ .logStrategy { priority, tag, message -> assertThat(tag).isEqualTo("custom") }
+ .build()
+
+ formatStrategy.log(Logger.VERBOSE, null, "message")
+ }
+}
diff --git a/QYlogger/src/test/java/com.orhanobut.logger/DiskLogAdapterTest.kt b/QYlogger/src/test/java/com.orhanobut.logger/DiskLogAdapterTest.kt
new file mode 100644
index 0000000..ef0b6b8
--- /dev/null
+++ b/QYlogger/src/test/java/com.orhanobut.logger/DiskLogAdapterTest.kt
@@ -0,0 +1,42 @@
+package com.orhanobut.logger
+
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.mockito.Mock
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations.initMocks
+
+class DiskLogAdapterTest {
+
+ @Mock private lateinit var formatStrategy: FormatStrategy
+
+ @Before fun setup() {
+ initMocks(this)
+ }
+
+ @Test fun isLoggableTrue() {
+ val logAdapter = DiskLogAdapter(formatStrategy)
+
+ assertThat(logAdapter.isLoggable(Logger.VERBOSE, "tag")).isTrue()
+ }
+
+ @Test fun isLoggableFalse() {
+ val logAdapter = object : DiskLogAdapter(formatStrategy) {
+ override fun isLoggable(priority: Int, tag: String?): Boolean {
+ return false
+ }
+ }
+
+ assertThat(logAdapter.isLoggable(Logger.VERBOSE, "tag")).isFalse()
+ }
+
+ @Test fun log() {
+ val logAdapter = DiskLogAdapter(formatStrategy)
+
+ logAdapter.log(Logger.VERBOSE, "tag", "message")
+
+ verify(formatStrategy).log(Logger.VERBOSE, "tag", "message")
+ }
+
+}
diff --git a/QYlogger/src/test/java/com.orhanobut.logger/DiskLogStrategyTest.kt b/QYlogger/src/test/java/com.orhanobut.logger/DiskLogStrategyTest.kt
new file mode 100644
index 0000000..b8c4acd
--- /dev/null
+++ b/QYlogger/src/test/java/com.orhanobut.logger/DiskLogStrategyTest.kt
@@ -0,0 +1,22 @@
+package com.orhanobut.logger
+
+import android.os.Handler
+
+import org.junit.Test
+
+import com.orhanobut.logger.Logger.DEBUG
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.verify
+
+class DiskLogStrategyTest {
+
+ @Test fun log() {
+ val handler = mock(Handler::class.java)
+ val logStrategy = DiskLogStrategy(handler)
+
+ logStrategy.log(DEBUG, "tag", "message")
+
+ verify(handler).sendMessage(handler.obtainMessage(DEBUG, "message"))
+ }
+
+}
\ No newline at end of file
diff --git a/QYlogger/src/test/java/com.orhanobut.logger/LogAssert.kt b/QYlogger/src/test/java/com.orhanobut.logger/LogAssert.kt
new file mode 100644
index 0000000..0fda48d
--- /dev/null
+++ b/QYlogger/src/test/java/com.orhanobut.logger/LogAssert.kt
@@ -0,0 +1,70 @@
+package com.orhanobut.logger
+
+import com.google.common.truth.Truth.assertThat
+
+internal class LogAssert(private val items: MutableList, tag: String?, private val priority: Int) {
+
+ private val tag: String
+
+ private var index = 0
+
+ init {
+ this.tag = tag ?: DEFAULT_TAG
+ }
+
+ fun hasTopBorder(): LogAssert {
+ return hasLog(priority, tag, TOP_BORDER)
+ }
+
+ fun hasBottomBorder(): LogAssert {
+ return hasLog(priority, tag, BOTTOM_BORDER)
+ }
+
+ fun hasMiddleBorder(): LogAssert {
+ return hasLog(priority, tag, MIDDLE_BORDER)
+ }
+
+ fun hasThread(threadName: String): LogAssert {
+ return hasLog(priority, tag, "$HORIZONTAL_LINE Thread: $threadName")
+ }
+
+ fun hasMethodInfo(methodInfo: String): LogAssert {
+ return hasLog(priority, tag, "$HORIZONTAL_LINE $methodInfo")
+ }
+
+ fun hasMessage(message: String): LogAssert {
+ return hasLog(priority, tag, "$HORIZONTAL_LINE $message")
+ }
+
+ private fun hasLog(priority: Int, tag: String, message: String): LogAssert = apply {
+ val item = items[index++]
+ assertThat(item.priority).isEqualTo(priority)
+ assertThat(item.tag).isEqualTo(tag)
+ assertThat(item.message).isEqualTo(message)
+ }
+
+ fun skip(): LogAssert = apply {
+ index++
+ }
+
+ fun hasNoMoreMessages() {
+ assertThat(items).hasSize(index)
+ items.clear()
+ }
+
+ internal class LogItem(val priority: Int, val tag: String, val message: String)
+
+ companion object {
+ private const val DEFAULT_TAG = "PRETTY_LOGGER"
+
+ private const val TOP_LEFT_CORNER = '┌'
+ private const val BOTTOM_LEFT_CORNER = '└'
+ private const val MIDDLE_CORNER = '├'
+ private const val HORIZONTAL_LINE = '│'
+ private const val DOUBLE_DIVIDER = "────────────────────────────────────────────────────────"
+ private const val SINGLE_DIVIDER = "┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄"
+ private val TOP_BORDER = TOP_LEFT_CORNER + DOUBLE_DIVIDER + DOUBLE_DIVIDER
+ private val BOTTOM_BORDER = BOTTOM_LEFT_CORNER + DOUBLE_DIVIDER + DOUBLE_DIVIDER
+ private val MIDDLE_BORDER = MIDDLE_CORNER + SINGLE_DIVIDER + SINGLE_DIVIDER
+ }
+}
diff --git a/QYlogger/src/test/java/com.orhanobut.logger/LogcatLogStrategyTest.kt b/QYlogger/src/test/java/com.orhanobut.logger/LogcatLogStrategyTest.kt
new file mode 100644
index 0000000..a005219
--- /dev/null
+++ b/QYlogger/src/test/java/com.orhanobut.logger/LogcatLogStrategyTest.kt
@@ -0,0 +1,35 @@
+package com.orhanobut.logger
+
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.annotation.Config
+import org.robolectric.shadows.ShadowLog
+
+import com.google.common.truth.Truth.assertThat
+import com.orhanobut.logger.Logger.DEBUG
+
+@RunWith(RobolectricTestRunner::class)
+class LogcatLogStrategyTest {
+
+ @Test fun log() {
+ val logStrategy = LogcatLogStrategy()
+
+ logStrategy.log(DEBUG, "tag", "message")
+
+ val logItems = ShadowLog.getLogs()
+ assertThat(logItems[0].type).isEqualTo(DEBUG)
+ assertThat(logItems[0].msg).isEqualTo("message")
+ assertThat(logItems[0].tag).isEqualTo("tag")
+ }
+
+ @Test fun logWithNullTag() {
+ val logStrategy = LogcatLogStrategy()
+
+ logStrategy.log(DEBUG, null, "message")
+
+ val logItems = ShadowLog.getLogs()
+ assertThat(logItems[0].tag).isEqualTo(LogcatLogStrategy.DEFAULT_TAG)
+ }
+
+}
diff --git a/QYlogger/src/test/java/com.orhanobut.logger/LoggerPrinterTest.kt b/QYlogger/src/test/java/com.orhanobut.logger/LoggerPrinterTest.kt
new file mode 100644
index 0000000..6ebb556
--- /dev/null
+++ b/QYlogger/src/test/java/com.orhanobut.logger/LoggerPrinterTest.kt
@@ -0,0 +1,239 @@
+package com.orhanobut.logger
+
+import com.orhanobut.logger.Logger.*
+import org.junit.Before
+import org.junit.Test
+import org.mockito.Matchers.any
+import org.mockito.Matchers.contains
+import org.mockito.Matchers.eq
+import org.mockito.Matchers.isNull
+import org.mockito.Mock
+import org.mockito.Mockito.`when`
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.never
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.verifyZeroInteractions
+import org.mockito.MockitoAnnotations.initMocks
+import java.util.*
+
+class LoggerPrinterTest {
+
+ private val printer = LoggerPrinter()
+
+ @Mock private lateinit var adapter: LogAdapter
+
+ @Before fun setup() {
+ initMocks(this)
+ `when`(adapter!!.isLoggable(any(Int::class.java), any(String::class.java))).thenReturn(true)
+ printer.addAdapter(adapter!!)
+ }
+
+ @Test fun logDebug() {
+ printer.d("message %s", "sent")
+
+ verify(adapter).log(DEBUG, null, "message sent")
+ }
+
+ @Test fun logError() {
+ printer.e("message %s", "sent")
+
+ verify(adapter).log(ERROR, null, "message sent")
+ }
+
+ @Test fun logErrorWithThrowable() {
+ val throwable = Throwable("exception")
+
+ printer.e(throwable, "message %s", "sent")
+
+ verify(adapter).log(eq(ERROR), isNull(String::class.java), contains("message sent : java.lang.Throwable: exception"))
+ }
+
+ @Test fun logWarning() {
+ printer.w("message %s", "sent")
+
+ verify(adapter).log(WARN, null, "message sent")
+ }
+
+ @Test fun logInfo() {
+ printer.i("message %s", "sent")
+
+ verify(adapter).log(INFO, null, "message sent")
+ }
+
+ @Test fun logWtf() {
+ printer.wtf("message %s", "sent")
+
+ verify(adapter).log(ASSERT, null, "message sent")
+ }
+
+ @Test fun logVerbose() {
+ printer.v("message %s", "sent")
+
+ verify(adapter).log(VERBOSE, null, "message sent")
+ }
+
+ @Test fun oneTimeTag() {
+ printer.t("tag").d("message")
+
+ verify(adapter).log(DEBUG, "tag", "message")
+ }
+
+ @Test fun logObject() {
+ val `object` = "Test"
+
+ printer.d(`object`)
+
+ verify(adapter).log(DEBUG, null, "Test")
+ }
+
+ @Test fun logArray() {
+ val `object` = intArrayOf(1, 6, 7, 30, 33)
+
+ printer.d(`object`)
+
+ verify(adapter).log(DEBUG, null, "[1, 6, 7, 30, 33]")
+ }
+
+ @Test fun logStringArray() {
+ val `object` = arrayOf("a", "b", "c")
+
+ printer.d(`object`)
+
+ verify(adapter).log(DEBUG, null, "[a, b, c]")
+ }
+
+ @Test fun logMultiDimensionArray() {
+ val doubles = arrayOf(doubleArrayOf(1.0, 6.0), doubleArrayOf(1.2, 33.0))
+
+ printer.d(doubles)
+
+ verify(adapter).log(DEBUG, null, "[[1.0, 6.0], [1.2, 33.0]]")
+ }
+
+ @Test fun logList() {
+ val list = Arrays.asList("foo", "bar")
+ printer.d(list)
+
+ verify(adapter).log(DEBUG, null, list.toString())
+ }
+
+ @Test fun logMap() {
+ val map = HashMap()
+ map["key"] = "value"
+ map["key2"] = "value2"
+
+ printer.d(map)
+
+ verify(adapter).log(DEBUG, null, map.toString())
+ }
+
+ @Test fun logSet() {
+ val set = HashSet()
+ set.add("key")
+ set.add("key1")
+
+ printer.d(set)
+
+ verify(adapter).log(DEBUG, null, set.toString())
+ }
+
+ @Test fun logJsonObject() {
+ printer.json(" {\"key\":3}")
+
+ verify(adapter).log(DEBUG, null, "{\"key\": 3}")
+ }
+
+ @Test fun logJsonArray() {
+ printer.json("[{\"key\":3}]")
+
+ verify(adapter).log(DEBUG, null, "[{\"key\": 3}]")
+ }
+
+
+ @Test fun logInvalidJsonObject() {
+ printer.json("no json")
+ printer.json("{ missing end")
+
+ verify(adapter, times(2)).log(ERROR, null, "Invalid Json")
+ }
+
+ @Test fun jsonLogEmptyOrNull() {
+ printer.json(null)
+ printer.json("")
+
+ verify(adapter, times(2)).log(DEBUG, null, "Empty/Null json content")
+ }
+
+ @Test fun xmlLog() {
+ printer.xml("Test")
+
+ verify(adapter).log(DEBUG, null,
+ "\nTest\n")
+ }
+
+ @Test fun invalidXmlLog() {
+ printer.xml("xml>Test")
+
+ verify(adapter).log(ERROR, null, "Invalid xml")
+ }
+
+ @Test fun xmlLogNullOrEmpty() {
+ printer.xml(null)
+ printer.xml("")
+
+ verify(adapter, times(2)).log(DEBUG, null, "Empty/Null xml content")
+ }
+
+ @Test fun clearLogAdapters() {
+ printer.clearLogAdapters()
+
+ printer.d("")
+
+ verifyZeroInteractions(adapter)
+ }
+
+ @Test fun addAdapter() {
+ printer.clearLogAdapters()
+ val adapter1 = mock(LogAdapter::class.java)
+ val adapter2 = mock(LogAdapter::class.java)
+
+ printer.addAdapter(adapter1)
+ printer.addAdapter(adapter2)
+
+ printer.d("message")
+
+ verify(adapter1).isLoggable(DEBUG, null)
+ verify(adapter2).isLoggable(DEBUG, null)
+ }
+
+ @Test fun doNotLogIfNotLoggable() {
+ printer.clearLogAdapters()
+ val adapter1 = mock(LogAdapter::class.java)
+ `when`(adapter1.isLoggable(DEBUG, null)).thenReturn(false)
+
+ val adapter2 = mock(LogAdapter::class.java)
+ `when`(adapter2.isLoggable(DEBUG, null)).thenReturn(true)
+
+ printer.addAdapter(adapter1)
+ printer.addAdapter(adapter2)
+
+ printer.d("message")
+
+ verify(adapter1, never()).log(DEBUG, null, "message")
+ verify(adapter2).log(DEBUG, null, "message")
+ }
+
+ @Test fun logWithoutMessageAndThrowable() {
+ printer.log(DEBUG, null, null, null)
+
+ verify(adapter).log(DEBUG, null, "Empty/NULL log message")
+ }
+
+ @Test fun logWithOnlyThrowableWithoutMessage() {
+ val throwable = Throwable("exception")
+ printer.log(DEBUG, null, null, throwable)
+
+ verify(adapter).log(eq(DEBUG), isNull(String::class.java), contains("java.lang.Throwable: exception"))
+ }
+}
\ No newline at end of file
diff --git a/QYlogger/src/test/java/com.orhanobut.logger/LoggerTest.kt b/QYlogger/src/test/java/com.orhanobut.logger/LoggerTest.kt
new file mode 100644
index 0000000..d2a0afe
--- /dev/null
+++ b/QYlogger/src/test/java/com.orhanobut.logger/LoggerTest.kt
@@ -0,0 +1,112 @@
+package com.orhanobut.logger
+
+import org.junit.Before
+import org.junit.Test
+import org.mockito.Mock
+
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
+import org.mockito.MockitoAnnotations.initMocks
+
+class LoggerTest {
+
+ @Mock private lateinit var printer: Printer
+
+ @Before fun setup() {
+ initMocks(this)
+
+ Logger.printer(printer)
+ }
+
+ @Test fun log() {
+ val throwable = Throwable()
+ Logger.log(Logger.VERBOSE, "tag", "message", throwable)
+
+ verify(printer).log(Logger.VERBOSE, "tag", "message", throwable)
+ }
+
+ @Test fun debugLog() {
+ Logger.d("message %s", "arg")
+
+ verify(printer).d("message %s", "arg")
+ }
+
+ @Test fun verboseLog() {
+ Logger.v("message %s", "arg")
+
+ verify(printer).v("message %s", "arg")
+ }
+
+ @Test fun warningLog() {
+ Logger.w("message %s", "arg")
+
+ verify(printer).w("message %s", "arg")
+ }
+
+ @Test fun errorLog() {
+ Logger.e("message %s", "arg")
+
+ verify(printer).e(null as Throwable?, "message %s", "arg")
+ }
+
+ @Test fun errorLogWithThrowable() {
+ val throwable = Throwable("throwable")
+ Logger.e(throwable, "message %s", "arg")
+
+ verify(printer).e(throwable, "message %s", "arg")
+ }
+
+ @Test fun infoLog() {
+ Logger.i("message %s", "arg")
+
+ verify(printer).i("message %s", "arg")
+ }
+
+ @Test fun wtfLog() {
+ Logger.wtf("message %s", "arg")
+
+ verify(printer).wtf("message %s", "arg")
+ }
+
+ @Test fun logObject() {
+ val `object` = Any()
+ Logger.d(`object`)
+
+ verify(printer).d(`object`)
+ }
+
+ @Test fun jsonLog() {
+ Logger.json("json")
+
+ verify(printer).json("json")
+ }
+
+ @Test fun xmlLog() {
+ Logger.xml("xml")
+
+ verify(printer).xml("xml")
+ }
+
+ @Test fun oneTimeTag() {
+ `when`(printer.t("tag")).thenReturn(printer)
+
+ Logger.t("tag").d("message")
+
+ verify(printer).t("tag")
+ verify(printer).d("message")
+ }
+
+ @Test fun addAdapter() {
+ val adapter = mock(LogAdapter::class.java)
+ Logger.addLogAdapter(adapter)
+
+ verify(printer).addAdapter(adapter)
+ }
+
+ @Test fun clearLogAdapters() {
+ Logger.clearLogAdapters()
+
+ verify(printer).clearLogAdapters()
+ }
+}
diff --git a/QYlogger/src/test/java/com.orhanobut.logger/PrettyFormatStrategyTest.kt b/QYlogger/src/test/java/com.orhanobut.logger/PrettyFormatStrategyTest.kt
new file mode 100644
index 0000000..417fbf2
--- /dev/null
+++ b/QYlogger/src/test/java/com.orhanobut.logger/PrettyFormatStrategyTest.kt
@@ -0,0 +1,185 @@
+package com.orhanobut.logger
+
+import org.junit.Test
+
+import java.util.ArrayList
+
+import com.orhanobut.logger.Logger.DEBUG
+
+class PrettyFormatStrategyTest {
+
+ private val threadName = Thread.currentThread().name
+ private val logStrategy = MockLogStrategy()
+ private val builder = PrettyFormatStrategy.newBuilder().logStrategy(logStrategy)
+
+ //TODO: Check the actual method info
+ @Test fun defaultLog() {
+ val formatStrategy = builder.build()
+
+ formatStrategy.log(DEBUG, null, "message")
+
+ assertLog(DEBUG)
+ .hasTopBorder()
+ .hasThread(threadName)
+ .hasMiddleBorder()
+ .skip()
+ .skip()
+ .hasMiddleBorder()
+ .hasMessage("message")
+ .hasBottomBorder()
+ .hasNoMoreMessages()
+ }
+
+ @Test fun logWithoutThreadInfo() {
+ val formatStrategy = builder.showThreadInfo(false).build()
+
+ formatStrategy.log(DEBUG, null, "message")
+
+ assertLog(DEBUG)
+ .hasTopBorder()
+ .skip()
+ .skip()
+ .hasMiddleBorder()
+ .hasMessage("message")
+ .hasBottomBorder()
+ .hasNoMoreMessages()
+ }
+
+ @Test fun logWithoutMethodInfo() {
+ val formatStrategy = builder.methodCount(0).build()
+
+ formatStrategy.log(DEBUG, null, "message")
+
+ assertLog(DEBUG)
+ .hasTopBorder()
+ .hasThread(threadName)
+ .hasMiddleBorder()
+ .hasMessage("message")
+ .hasBottomBorder()
+ .hasNoMoreMessages()
+ }
+
+ @Test fun logWithOnlyMessage() {
+ val formatStrategy = builder
+ .methodCount(0)
+ .showThreadInfo(false)
+ .build()
+
+ formatStrategy.log(DEBUG, null, "message")
+
+ assertLog(DEBUG)
+ .hasTopBorder()
+ .hasMessage("message")
+ .hasBottomBorder()
+ .hasNoMoreMessages()
+ }
+
+ //TODO: Check the actual method info
+ @Test fun logWithCustomMethodOffset() {
+ val formatStrategy = builder
+ .methodOffset(2)
+ .showThreadInfo(false)
+ .build()
+
+ formatStrategy.log(DEBUG, null, "message")
+
+ assertLog(DEBUG)
+ .hasTopBorder()
+ .skip()
+ .skip()
+ .hasMiddleBorder()
+ .hasMessage("message")
+ .hasBottomBorder()
+ .hasNoMoreMessages()
+ }
+
+ @Test fun logWithCustomTag() {
+ val formatStrategy = builder
+ .tag("custom")
+ .build()
+
+ formatStrategy.log(DEBUG, null, "message")
+
+ assertLog("custom", DEBUG)
+ .hasTopBorder()
+ .hasThread(threadName)
+ .hasMiddleBorder()
+ .skip()
+ .skip()
+ .hasMiddleBorder()
+ .hasMessage("message")
+ .hasBottomBorder()
+ .hasNoMoreMessages()
+ }
+
+ @Test fun logWithOneTimeTag() {
+ val formatStrategy = builder
+ .tag("custom")
+ .build()
+
+ formatStrategy.log(DEBUG, "tag", "message")
+
+ assertLog("custom-tag", DEBUG)
+ .hasTopBorder()
+ .hasThread(threadName)
+ .hasMiddleBorder()
+ .skip()
+ .skip()
+ .hasMiddleBorder()
+ .hasMessage("message")
+ .hasBottomBorder()
+ .hasNoMoreMessages()
+ }
+
+ // TODO: assert values, for now this checks that Logger doesn't crash
+ @Test fun logWithExceedingMethodCount() {
+ val formatStrategy = builder
+ .methodCount(50)
+ .build()
+
+ formatStrategy.log(DEBUG, null, "message")
+ }
+
+ @Test fun logWithBigChunk() {
+ val formatStrategy = builder.build()
+
+ val chunk1 = StringBuilder()
+ for (i in 0..399) {
+ chunk1.append("1234567890")
+ }
+ val chunk2 = StringBuilder()
+ for (i in 0..9) {
+ chunk2.append("ABCDEFGD")
+ }
+
+ formatStrategy.log(DEBUG, null, chunk1.toString() + chunk2.toString())
+
+ assertLog(DEBUG)
+ .hasTopBorder()
+ .hasThread(threadName)
+ .hasMiddleBorder()
+ .skip()
+ .skip()
+ .hasMiddleBorder()
+ .hasMessage(chunk1.toString())
+ .hasMessage(chunk2.toString())
+ .hasBottomBorder()
+ .hasNoMoreMessages()
+ }
+
+ private class MockLogStrategy : LogStrategy {
+ internal var logItems: MutableList = ArrayList()
+
+ override fun log(priority: Int, tag: String?, message: String) {
+ logItems.add(LogAssert.LogItem(priority, tag ?: "", message))
+ }
+ }
+
+ private fun assertLog(priority: Int): LogAssert {
+ return assertLog(null, priority)
+ }
+
+ private fun assertLog(tag: String?, priority: Int): LogAssert {
+ return LogAssert(logStrategy.logItems, tag, priority)
+ }
+}
diff --git a/QYlogger/src/test/java/com.orhanobut.logger/UtilsTest.kt b/QYlogger/src/test/java/com.orhanobut.logger/UtilsTest.kt
new file mode 100644
index 0000000..2527142
--- /dev/null
+++ b/QYlogger/src/test/java/com.orhanobut.logger/UtilsTest.kt
@@ -0,0 +1,94 @@
+package com.orhanobut.logger
+
+import android.util.Log
+
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.annotation.Config
+
+import java.net.UnknownHostException
+
+import com.google.common.truth.Truth.assertThat
+
+@RunWith(RobolectricTestRunner::class)
+class UtilsTest {
+
+ @Test fun isEmpty() {
+ assertThat(Utils.isEmpty("")).isTrue()
+ assertThat(Utils.isEmpty(null)).isTrue()
+ }
+
+ @Test fun equals() {
+ assertThat(Utils.equals("a", "a")).isTrue()
+ assertThat(Utils.equals("as", "b")).isFalse()
+ assertThat(Utils.equals(null, "b")).isFalse()
+ assertThat(Utils.equals("a", null)).isFalse()
+ }
+
+ @Test fun getStackTraceString() {
+ val throwable = Throwable("test")
+ val androidTraceString = Log.getStackTraceString(throwable)
+ assertThat(Utils.getStackTraceString(throwable)).isEqualTo(androidTraceString)
+ }
+
+ @Test fun getStackTraceStringReturnsEmptyStringWithNull() {
+ assertThat(Utils.getStackTraceString(null)).isEqualTo("")
+ }
+
+ @Test fun getStackTraceStringReturnEmptyStringWithUnknownHostException() {
+ assertThat(Utils.getStackTraceString(UnknownHostException())).isEqualTo("")
+ }
+
+ @Test fun logLevels() {
+ assertThat(Utils.logLevel(Logger.DEBUG)).isEqualTo("DEBUG")
+ assertThat(Utils.logLevel(Logger.WARN)).isEqualTo("WARN")
+ assertThat(Utils.logLevel(Logger.INFO)).isEqualTo("INFO")
+ assertThat(Utils.logLevel(Logger.VERBOSE)).isEqualTo("VERBOSE")
+ assertThat(Utils.logLevel(Logger.ASSERT)).isEqualTo("ASSERT")
+ assertThat(Utils.logLevel(Logger.ERROR)).isEqualTo("ERROR")
+ assertThat(Utils.logLevel(100)).isEqualTo("UNKNOWN")
+ }
+
+ @Test fun objectToString() {
+ val `object` = "Test"
+
+ assertThat(Utils.toString(`object`)).isEqualTo("Test")
+ }
+
+ @Test fun toStringWithNull() {
+ assertThat(Utils.toString(null)).isEqualTo("null")
+ }
+
+ @Test fun primitiveArrayToString() {
+ val booleanArray = booleanArrayOf(true, false, true)
+ assertThat(Utils.toString(booleanArray)).isEqualTo("[true, false, true]")
+
+ val byteArray = byteArrayOf(1, 0, 1)
+ assertThat(Utils.toString(byteArray)).isEqualTo("[1, 0, 1]")
+
+ val charArray = charArrayOf('a', 'b', 'c')
+ assertThat(Utils.toString(charArray)).isEqualTo("[a, b, c]")
+
+ val shortArray = shortArrayOf(1, 3, 5)
+ assertThat(Utils.toString(shortArray)).isEqualTo("[1, 3, 5]")
+
+ val intArray = intArrayOf(1, 3, 5)
+ assertThat(Utils.toString(intArray)).isEqualTo("[1, 3, 5]")
+
+ val longArray = longArrayOf(1, 3, 5)
+ assertThat(Utils.toString(longArray)).isEqualTo("[1, 3, 5]")
+
+ val floatArray = floatArrayOf(1f, 3f, 5f)
+ assertThat(Utils.toString(floatArray)).isEqualTo("[1.0, 3.0, 5.0]")
+
+ val doubleArray = doubleArrayOf(1.0, 3.0, 5.0)
+ assertThat(Utils.toString(doubleArray)).isEqualTo("[1.0, 3.0, 5.0]")
+ }
+
+ @Test fun multiDimensionArrayToString() {
+ val `object` = arrayOf(intArrayOf(1, 2, 3), intArrayOf(4, 5, 6))
+
+ assertThat(Utils.toString(`object`)).isEqualTo("[[1, 2, 3], [4, 5, 6]]")
+ }
+}
\ No newline at end of file
diff --git a/app/.gitignore b/app/.gitignore
new file mode 100644
index 0000000..796b96d
--- /dev/null
+++ b/app/.gitignore
@@ -0,0 +1 @@
+/build
diff --git a/app/build.gradle b/app/build.gradle
new file mode 100644
index 0000000..640a2a5
--- /dev/null
+++ b/app/build.gradle
@@ -0,0 +1,29 @@
+apply plugin: 'com.android.application'
+
+android {
+ compileSdkVersion 27
+ defaultConfig {
+ applicationId "com.qj.logger"
+ minSdkVersion 21
+ targetSdkVersion 27
+ versionCode 1
+ versionName "1.0"
+ testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
+ }
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+}
+
+dependencies {
+ implementation fileTree(dir: 'libs', include: ['*.jar'])
+ implementation 'com.android.support:appcompat-v7:27.1.1'
+ implementation 'com.android.support.constraint:constraint-layout:1.1.3'
+ testImplementation 'junit:junit:4.12'
+ androidTestImplementation 'com.android.support.test:runner:1.0.2'
+ androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
+ compile project(path: ':QYlogger')
+}
diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro
new file mode 100644
index 0000000..f1b4245
--- /dev/null
+++ b/app/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
diff --git a/app/src/androidTest/java/com/qj/logger/ExampleInstrumentedTest.java b/app/src/androidTest/java/com/qj/logger/ExampleInstrumentedTest.java
new file mode 100644
index 0000000..b693582
--- /dev/null
+++ b/app/src/androidTest/java/com/qj/logger/ExampleInstrumentedTest.java
@@ -0,0 +1,26 @@
+package com.qj.logger;
+
+import android.content.Context;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import static org.junit.Assert.*;
+
+/**
+ * Instrumented test, which will execute on an Android device.
+ *
+ * @see Testing documentation
+ */
+@RunWith(AndroidJUnit4.class)
+public class ExampleInstrumentedTest {
+ @Test
+ public void useAppContext() throws Exception {
+ // Context of the app under test.
+ Context appContext = InstrumentationRegistry.getTargetContext();
+
+ assertEquals("com.qj.logger", appContext.getPackageName());
+ }
+}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..77606b4
--- /dev/null
+++ b/app/src/main/AndroidManifest.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml
new file mode 100644
index 0000000..c7bd21d
--- /dev/null
+++ b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml
new file mode 100644
index 0000000..d5fccc5
--- /dev/null
+++ b/app/src/main/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,170 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
new file mode 100644
index 0000000..0d8ad42
--- /dev/null
+++ b/app/src/main/res/layout/activity_main.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 0000000..eca70cf
--- /dev/null
+++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 0000000..eca70cf
--- /dev/null
+++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.png b/app/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000..a2f5908
Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
new file mode 100644
index 0000000..1b52399
Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.png b/app/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000..ff10afd
Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
new file mode 100644
index 0000000..115a4c7
Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/app/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..dcd3cd8
Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..459ca60
Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000..8ca12fe
Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..8e19b41
Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000..b824ebd
Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..4c19a13
Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
new file mode 100644
index 0000000..3ab3e9c
--- /dev/null
+++ b/app/src/main/res/values/colors.xml
@@ -0,0 +1,6 @@
+
+
+ #3F51B5
+ #303F9F
+ #FF4081
+
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
new file mode 100644
index 0000000..79f29d0
--- /dev/null
+++ b/app/src/main/res/values/strings.xml
@@ -0,0 +1,3 @@
+
+ QYLogger
+
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
new file mode 100644
index 0000000..5885930
--- /dev/null
+++ b/app/src/main/res/values/styles.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
diff --git a/app/src/test/java/com/qj/logger/ExampleUnitTest.java b/app/src/test/java/com/qj/logger/ExampleUnitTest.java
new file mode 100644
index 0000000..96c31c1
--- /dev/null
+++ b/app/src/test/java/com/qj/logger/ExampleUnitTest.java
@@ -0,0 +1,17 @@
+package com.qj.logger;
+
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * @see Testing documentation
+ */
+public class ExampleUnitTest {
+ @Test
+ public void addition_isCorrect() throws Exception {
+ assertEquals(4, 2 + 2);
+ }
+}
\ No newline at end of file
diff --git a/build.gradle b/build.gradle
new file mode 100644
index 0000000..97f22b3
--- /dev/null
+++ b/build.gradle
@@ -0,0 +1,27 @@
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+
+buildscript {
+
+ repositories {
+ google()
+ jcenter()
+ }
+ dependencies {
+ classpath 'com.android.tools.build:gradle:3.0.1'
+
+ classpath 'com.novoda:bintray-release:0.8.0'
+ // NOTE: Do not place your application dependencies here; they belong
+ // in the individual module build.gradle files
+ }
+}
+
+allprojects {
+ repositories {
+ google()
+ jcenter()
+ }
+}
+
+task clean(type: Delete) {
+ delete rootProject.buildDir
+}
diff --git a/gradle.properties b/gradle.properties
new file mode 100644
index 0000000..aac7c9b
--- /dev/null
+++ b/gradle.properties
@@ -0,0 +1,17 @@
+# Project-wide Gradle settings.
+
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+org.gradle.jvmargs=-Xmx1536m
+
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. More details, visit
+# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
+# org.gradle.parallel=true
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..13372ae
Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..f617b16
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Thu Jan 31 11:51:32 CST 2019
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-4.1-all.zip
diff --git a/gradlew b/gradlew
new file mode 100644
index 0000000..9d82f78
--- /dev/null
+++ b/gradlew
@@ -0,0 +1,160 @@
+#!/usr/bin/env bash
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn ( ) {
+ echo "$*"
+}
+
+die ( ) {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+esac
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+ JAVACMD=`cygpath --unix "$JAVACMD"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=$((i+1))
+ done
+ case $i in
+ (0) set -- ;;
+ (1) set -- "$args0" ;;
+ (2) set -- "$args0" "$args1" ;;
+ (3) set -- "$args0" "$args1" "$args2" ;;
+ (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
+function splitJvmOpts() {
+ JVM_OPTS=("$@")
+}
+eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
+JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
+
+exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
diff --git a/gradlew.bat b/gradlew.bat
new file mode 100644
index 0000000..8a0b282
--- /dev/null
+++ b/gradlew.bat
@@ -0,0 +1,90 @@
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS=
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windowz variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+if "%@eval[2+2]" == "4" goto 4NT_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+goto execute
+
+:4NT_args
+@rem Get arguments from the 4NT Shell from JP Software
+set CMD_LINE_ARGS=%$
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/settings.gradle b/settings.gradle
new file mode 100644
index 0000000..ad34526
--- /dev/null
+++ b/settings.gradle
@@ -0,0 +1 @@
+include ':app', ':QYlogger'