diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..2b75303
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,13 @@
+*.iml
+.gradle
+/local.properties
+/.idea/caches
+/.idea/libraries
+/.idea/modules.xml
+/.idea/workspace.xml
+/.idea/navEditor.xml
+/.idea/assetWizardSettings.xml
+.DS_Store
+/build
+/captures
+.externalNativeBuild
diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml
new file mode 100644
index 0000000..17b11b3
--- /dev/null
+++ b/.idea/codeStyles/Project.xml
@@ -0,0 +1,224 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ xmlns:android
+ ^$
+
+
+
+
+
+
+
+
+ xmlns:.*
+ ^$
+
+
+ BY_NAME
+
+
+
+
+
+
+ .*:id
+ http://schemas.android.com/apk/res/android
+
+
+
+
+
+
+
+
+ .*:name
+ http://schemas.android.com/apk/res/android
+
+
+
+
+
+
+
+
+ name
+ ^$
+
+
+
+
+
+
+
+
+ style
+ ^$
+
+
+
+
+
+
+
+
+ .*
+ ^$
+
+
+ BY_NAME
+
+
+
+
+
+
+ .*:layout_width
+ http://schemas.android.com/apk/res/android
+
+
+
+
+
+
+
+
+ .*:layout_height
+ http://schemas.android.com/apk/res/android
+
+
+
+
+
+
+
+
+ .*:layout_.*
+ http://schemas.android.com/apk/res/android
+
+
+ BY_NAME
+
+
+
+
+
+
+ .*:width
+ http://schemas.android.com/apk/res/android
+
+
+ BY_NAME
+
+
+
+
+
+
+ .*:height
+ http://schemas.android.com/apk/res/android
+
+
+ BY_NAME
+
+
+
+
+
+
+ .*
+ http://schemas.android.com/apk/res/android
+
+
+ BY_NAME
+
+
+
+
+
+
+ .*
+ .*
+
+
+ BY_NAME
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml
new file mode 100644
index 0000000..8f1a3b7
--- /dev/null
+++ b/.idea/codeStyles/codeStyleConfig.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/dictionaries/T_Yurkiv.xml b/.idea/dictionaries/T_Yurkiv.xml
new file mode 100644
index 0000000..57b586b
--- /dev/null
+++ b/.idea/dictionaries/T_Yurkiv.xml
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/.idea/encodings.xml b/.idea/encodings.xml
new file mode 100644
index 0000000..ada92a5
--- /dev/null
+++ b/.idea/encodings.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/gradle.xml b/.idea/gradle.xml
new file mode 100644
index 0000000..2996d53
--- /dev/null
+++ b/.idea/gradle.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
new file mode 100644
index 0000000..3238d79
--- /dev/null
+++ b/.idea/misc.xml
@@ -0,0 +1,68 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Android
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml
new file mode 100644
index 0000000..7f68460
--- /dev/null
+++ b/.idea/runConfigurations.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 0000000..94a25f7
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,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..3add977
--- /dev/null
+++ b/app/build.gradle
@@ -0,0 +1,25 @@
+apply plugin: 'com.android.application'
+
+android {
+ compileSdkVersion 29
+ buildToolsVersion "29.0.0"
+ defaultConfig {
+ applicationId "com.devlight.logcat"
+ minSdkVersion 14
+ targetSdkVersion 29
+ versionCode 1
+ versionName "1.0"
+ }
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+ }
+ }
+}
+
+dependencies {
+ implementation fileTree(dir: 'libs', include: ['*.jar'])
+ implementation 'androidx.appcompat:appcompat:1.0.2'
+ implementation project(path: ':logcat')
+}
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/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..5427d04
--- /dev/null
+++ b/app/src/main/AndroidManifest.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/java/com/devlight/logcat_app/MainActivity.java b/app/src/main/java/com/devlight/logcat_app/MainActivity.java
new file mode 100644
index 0000000..9233778
--- /dev/null
+++ b/app/src/main/java/com/devlight/logcat_app/MainActivity.java
@@ -0,0 +1,27 @@
+package com.devlight.logcat_app;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.View;
+
+import androidx.appcompat.app.AppCompatActivity;
+
+import com.devlight.logcat.LogcatActivity;
+
+public class MainActivity extends AppCompatActivity {
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_main);
+
+ findViewById(R.id.btn_main_open_logcat).setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ Intent intent = new Intent(MainActivity.this, LogcatActivity.class);
+ intent.putExtra(LogcatActivity.BUFFER_SIZE_EXTRA, 1000);
+ startActivity(intent);
+ }
+ });
+ }
+}
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..dd73f13
--- /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..fe5102e
--- /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..22c42bb
--- /dev/null
+++ b/app/src/main/res/layout/activity_main.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
\ No newline at end of file
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..65bd9d3
--- /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..65bd9d3
--- /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..898f3ed
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..dffca36
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..64ba76f
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..dae5e08
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..e5ed465
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..14ed0af
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..b0907ca
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..d8ae031
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..2c18de9
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..beed3cd
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..a491964
--- /dev/null
+++ b/app/src/main/res/values/colors.xml
@@ -0,0 +1,6 @@
+
+
+ #008577
+ #00574B
+ #D81B60
+
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
new file mode 100644
index 0000000..1541ed7
--- /dev/null
+++ b/app/src/main/res/values/strings.xml
@@ -0,0 +1,3 @@
+
+ Logcat
+
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
new file mode 100644
index 0000000..a00f57d
--- /dev/null
+++ b/app/src/main/res/values/styles.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
diff --git a/build.gradle b/build.gradle
new file mode 100644
index 0000000..d357a30
--- /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.4.2'
+
+ // 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..199d16e
--- /dev/null
+++ b/gradle.properties
@@ -0,0 +1,20 @@
+# 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
+# AndroidX package structure to make it clearer which packages are bundled with the
+# Android operating system, and which are packaged with your app's APK
+# https://developer.android.com/topic/libraries/support-library/androidx-rn
+android.useAndroidX=true
+# Automatically convert third-party libraries to use AndroidX
+android.enableJetifier=true
+
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..f6b961f
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..b55b7ee
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Wed Jul 24 16:44:10 EEST 2019
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-5.4-all.zip
diff --git a/gradlew b/gradlew
new file mode 100755
index 0000000..cccdd3d
--- /dev/null
+++ b/gradlew
@@ -0,0 +1,172 @@
+#!/usr/bin/env sh
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# 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
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+# 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
+nonstop=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+ NONSTOP* )
+ nonstop=true
+ ;;
+esac
+
+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" -a "$nonstop" = "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
+
+# Escape application args
+save () {
+ for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+ echo " "
+}
+APP_ARGS=$(save "$@")
+
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+
+# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
+if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
+ cd "$(dirname "$0")"
+fi
+
+exec "$JAVACMD" "$@"
diff --git a/gradlew.bat b/gradlew.bat
new file mode 100644
index 0000000..f955316
--- /dev/null
+++ b/gradlew.bat
@@ -0,0 +1,84 @@
+@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
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@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=
+
+@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 Windows variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_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=%*
+
+: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/logcat/.gitignore b/logcat/.gitignore
new file mode 100644
index 0000000..796b96d
--- /dev/null
+++ b/logcat/.gitignore
@@ -0,0 +1 @@
+/build
diff --git a/logcat/build.gradle b/logcat/build.gradle
new file mode 100644
index 0000000..3384dfb
--- /dev/null
+++ b/logcat/build.gradle
@@ -0,0 +1,27 @@
+apply plugin: 'com.android.library'
+
+android {
+ compileSdkVersion 29
+ buildToolsVersion "29.0.0"
+
+ defaultConfig {
+ minSdkVersion 14
+ targetSdkVersion 29
+ versionCode 1
+ versionName "1.0"
+
+ vectorDrawables.useSupportLibrary = true
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+ }
+ }
+}
+
+dependencies {
+ implementation 'androidx.appcompat:appcompat:1.0.2'
+ implementation 'androidx.recyclerview:recyclerview:1.1.0-beta01'
+}
diff --git a/logcat/proguard-rules.pro b/logcat/proguard-rules.pro
new file mode 100644
index 0000000..f1b4245
--- /dev/null
+++ b/logcat/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/logcat/src/main/AndroidManifest.xml b/logcat/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..c9666fa
--- /dev/null
+++ b/logcat/src/main/AndroidManifest.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/logcat/src/main/java/com/devlight/logcat/LogcatActivity.java b/logcat/src/main/java/com/devlight/logcat/LogcatActivity.java
new file mode 100644
index 0000000..1bd90a0
--- /dev/null
+++ b/logcat/src/main/java/com/devlight/logcat/LogcatActivity.java
@@ -0,0 +1,469 @@
+package com.devlight.logcat;
+
+import android.content.ClipData;
+import android.content.ClipboardManager;
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.os.Bundle;
+import android.text.Editable;
+import android.text.TextUtils;
+import android.text.TextWatcher;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.animation.AccelerateInterpolator;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.CheckBox;
+import android.widget.CompoundButton;
+import android.widget.EditText;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.appcompat.widget.PopupMenu;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.devlight.logcat.adapter.LogRvAdapter;
+import com.devlight.logcat.model.Logcat;
+import com.devlight.logcat.model.Trace;
+import com.devlight.logcat.model.TraceBuffer;
+import com.devlight.logcat.model.TraceLevel;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.TreeSet;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.regex.PatternSyntaxException;
+
+/**
+ * @author Taras Yurkiv
+ * Email TYurkiv1995@gmail.com
+ * @since 19.07.2019
+ */
+public class LogcatActivity extends AppCompatActivity implements
+ Logcat.Listener,
+ View.OnClickListener,
+ CompoundButton.OnCheckedChangeListener,
+ PopupMenu.OnMenuItemClickListener,
+ LogRvAdapter.OnSelectedTraceRangeChangeListener {
+
+ public static final String BUFFER_SIZE_EXTRA = "BUFFER_SIZE_EXTRA";
+ private final long animDuration = 200;
+
+ private View progressView;
+ private RecyclerView rvLog;
+ private LinearLayoutManager linearLayoutManager;
+ private LogRvAdapter logRvAdapter;
+
+ private View btnDown;
+ private TextView txtTraceLevel;
+ private EditText etxtFilter;
+ private View btnClearFilter;
+ private CheckBox cbRegex;
+ private ViewGroup shareContainer;
+ private View btnShare;
+ private View btnCopy;
+ private View btnSelectAllBetween;
+ private View btnCloseSelect;
+
+ private final Logcat log = new Logcat();
+
+ private final TraceBuffer traceBuffer = new TraceBuffer(2500);
+ private boolean isInitialized;
+ private TraceLevel traceLevel = TraceLevel.VERBOSE;
+
+ private boolean hasSelectedTrace;
+ private List waitingTraces = new ArrayList<>();
+
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ setContentView(R.layout.activity_logcat);
+
+ Bundle bundle = getIntent().getExtras();
+ if (bundle != null) {
+ int bufferSize = bundle.getInt(BUFFER_SIZE_EXTRA, 2500);
+ if (bufferSize < 0) bufferSize = 2500;
+ traceBuffer.setBufferSize(bufferSize);
+ }
+ progressView = findViewById(R.id.pw_log_cat);
+ rvLog = findViewById(R.id.rv_log_cat);
+ linearLayoutManager = (LinearLayoutManager) rvLog.getLayoutManager();
+ logRvAdapter = new LogRvAdapter(true, true, this);
+ rvLog.setAdapter(logRvAdapter);
+
+ CheckBox cbTime = findViewById(R.id.cb_log_cat_time);
+ cbTime.setOnCheckedChangeListener(this);
+ CheckBox cbTag = findViewById(R.id.cb_log_cat_tag);
+ cbTag.setOnCheckedChangeListener(this);
+ CheckBox cbWrapLog = findViewById(R.id.cb_log_cat_wrap_log);
+ cbWrapLog.setOnCheckedChangeListener(this);
+
+ btnDown = findViewById(R.id.fab_log_cat);
+ btnDown.setOnClickListener(this);
+ showFab(false, false);
+ setListWidth(false, false);
+
+ rvLog.addOnScrollListener(new RecyclerView.OnScrollListener() {
+ @Override
+ public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
+ if (dy > 0 && rvLog.canScrollVertically(1)) {
+ showFab(true, true);
+ } else showFab(false, true);
+ }
+
+ @Override
+ public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
+ if (!rvLog.canScrollVertically(1)) showFab(false, true);
+ }
+ });
+
+ txtTraceLevel = findViewById(R.id.txt_log_cat_trace_level);
+ txtTraceLevel.setOnClickListener(this);
+ txtTraceLevel.setText(traceLevel.getName());
+
+ etxtFilter = findViewById(R.id.etxt_log_cat_filter);
+ btnClearFilter = findViewById(R.id.btn_log_cat_clear_filter_field);
+ btnClearFilter.setClickable(false);
+ btnClearFilter.setAlpha(0);
+ cbRegex = findViewById(R.id.cb_log_cat_regex);
+
+ etxtFilter.addTextChangedListener(new TextWatcher() {
+ String prevText;
+
+ @Override
+ public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {
+ if (charSequence == null) prevText = null;
+ else prevText = charSequence.toString();
+
+ if (TextUtils.isEmpty(prevText)) prevText = null;
+ }
+
+ @Override
+ public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
+ }
+
+ @Override
+ public void afterTextChanged(Editable editable) {
+ String searchText = String.valueOf(editable);
+ filter();
+ if (TextUtils.isEmpty(searchText) && prevText != null) {
+ btnClearFilter.setClickable(false);
+ btnClearFilter.animate().alpha(0).setDuration(animDuration);
+ } else if (!TextUtils.isEmpty(searchText) && prevText == null) {
+ btnClearFilter.setClickable(true);
+ btnClearFilter.animate().alpha(1).setDuration(animDuration);
+ }
+ }
+ });
+ btnClearFilter.setOnClickListener(this);
+ cbRegex.setOnCheckedChangeListener(this);
+
+ shareContainer = findViewById(R.id.log_cat_share_container);
+ shareContainer.setEnabled(false);
+
+ btnShare = findViewById(R.id.btn_log_cat_share);
+ btnCopy = findViewById(R.id.btn_log_cat_copy);
+ btnSelectAllBetween = findViewById(R.id.btn_log_cat_select_all_between);
+ btnCloseSelect = findViewById(R.id.btn_log_cat_close_select);
+ btnShare.setOnClickListener(this);
+ btnCopy.setOnClickListener(this);
+ btnSelectAllBetween.setOnClickListener(this);
+ btnCloseSelect.setOnClickListener(this);
+ }
+
+ private void showFab(boolean show, boolean animate) {
+ if (animate && show == btnDown.isEnabled()) return;
+
+ btnDown.setEnabled(show);
+ if (animate) {
+ btnDown.animate()
+ .scaleX(show ? 1 : 0)
+ .scaleY(show ? 1 : 0)
+ .setInterpolator(new AccelerateInterpolator())
+ .setDuration(animDuration);
+ } else {
+ btnDown.clearAnimation();
+ btnDown.setScaleX(show ? 1 : 0);
+ btnDown.setScaleY(show ? 1 : 0);
+ }
+ }
+
+ private void showShareContainer(boolean show) {
+ etxtFilter.setEnabled(!show);
+ txtTraceLevel.setEnabled(!show);
+ cbRegex.setEnabled(!show);
+
+ btnShare.setEnabled(show);
+ btnCopy.setEnabled(show);
+ btnSelectAllBetween.setEnabled(show);
+ btnCloseSelect.setEnabled(show);
+
+ if (show != shareContainer.isEnabled()) {
+ shareContainer.setEnabled(show);
+ shareContainer.animate()
+ .translationY(show ? 0 : shareContainer.getHeight())
+ .setDuration(animDuration);
+ }
+
+ if (!show) {
+ hideKeyboard();
+ }
+ }
+
+ private void showSelectAllBetweenButton(boolean show) {
+ if (show != btnSelectAllBetween.isClickable()) {
+ btnSelectAllBetween.setClickable(show);
+ btnSelectAllBetween.animate()
+ .alpha(show ? 1 : 0)
+ .setDuration(animDuration);
+ }
+ }
+
+ private void hideKeyboard() {
+ final View view = getCurrentFocus();
+ if (view != null) {
+ view.clearFocus();
+ InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
+ if (inputMethodManager != null) {
+ inputMethodManager.hideSoftInputFromWindow(view.getWindowToken(), 0);
+ }
+ }
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ if (!isInitialized) {
+ isInitialized = true;
+ log.setListener(this);
+ log.startReading();
+ }
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ if (isInitialized) {
+ isInitialized = false;
+ log.stopReading();
+ log.setListener(null);
+ }
+ }
+
+ @Override
+ public void onClick(View v) {
+ int i = v.getId();
+ if (i == R.id.fab_log_cat) {
+ rvLog.scrollToPosition(logRvAdapter.getItemCount() - 1);
+ } else if (i == R.id.txt_log_cat_trace_level) {
+ PopupMenu popup = new PopupMenu(this, txtTraceLevel);
+ popup.getMenuInflater().inflate(R.menu.popup_trace_level, popup.getMenu());
+ popup.setOnMenuItemClickListener(this);
+ popup.show();
+ } else if (i == R.id.btn_log_cat_clear_filter_field) {
+ etxtFilter.setText("");
+ } else if (i == R.id.btn_log_cat_share) {
+ Intent intent = new Intent(Intent.ACTION_SEND);
+ intent.setType("text/plain");
+ intent.putExtra(Intent.EXTRA_TEXT, logRvAdapter.getSelectedTracesAsString());
+ startActivity(Intent.createChooser(intent, ""));
+
+ logRvAdapter.clearSelectedItems(true);
+ } else if (i == R.id.btn_log_cat_copy) {
+ ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
+ if (clipboard != null) {
+ ClipData clip = ClipData.newPlainText("Log", logRvAdapter.getSelectedTracesAsString());
+ clipboard.setPrimaryClip(clip);
+
+ Toast.makeText(this, R.string.title_log_copied, Toast.LENGTH_SHORT).show();
+ }
+
+ logRvAdapter.clearSelectedItems(true);
+ } else if (i == R.id.btn_log_cat_select_all_between) {
+ logRvAdapter.selectAllBetween(true);
+ notifyWithSaveUpScroll();
+ } else if (i == R.id.btn_log_cat_close_select) {
+ logRvAdapter.clearSelectedItems(true);
+ }
+ }
+
+ @Override
+ public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
+ int i = buttonView.getId();
+ if (i == R.id.cb_log_cat_time) {
+ logRvAdapter.setShowTime(isChecked);
+ notifyWithSaveUpScroll();
+ } else if (i == R.id.cb_log_cat_tag) {
+ logRvAdapter.setShowTag(isChecked);
+ notifyWithSaveUpScroll();
+ } else if (i == R.id.cb_log_cat_wrap_log) {
+ setListWidth(isChecked, true);
+ } else if (i == R.id.cb_log_cat_regex) {
+ if (!TextUtils.isEmpty(etxtFilter.getText())) {
+ filter();
+ }
+ }
+ }
+
+ @Override
+ public boolean onMenuItemClick(MenuItem item) {
+ TraceLevel traceLevel = TraceLevel.getTraceLevel(String.valueOf(item.getTitle()));
+ if (this.traceLevel != traceLevel) {
+ this.traceLevel = traceLevel;
+ txtTraceLevel.setText(traceLevel.getName());
+
+ filter();
+ }
+ return false;
+ }
+
+ @Override
+ public void onNewTraces(List traces) {
+ progressView.setVisibility(View.GONE);
+
+ if (hasSelectedTrace) {
+ waitingTraces.addAll(traces);
+ return;
+ }
+ boolean canScrollDown = rvLog.canScrollVertically(1);
+ boolean canScrollUp = rvLog.canScrollVertically(-1);
+
+ int position = linearLayoutManager.findFirstVisibleItemPosition();
+ Trace firstTrace = logRvAdapter.getItem(position);
+
+ View startView = rvLog.getChildAt(0);
+ int offset = (startView == null) ? 0 : (startView.getTop());
+
+ traceBuffer.add(traces);
+
+ filter(etxtFilter.getText().toString(), cbRegex.isChecked(), firstTrace, offset, canScrollDown, canScrollUp);
+ }
+
+ @Override
+ public void onSelectedTracesRangeChanged(TreeSet selectedRange) {
+ hasSelectedTrace = !selectedRange.isEmpty();
+
+ if (!hasSelectedTrace) {
+ showSelectAllBetweenButton(false);
+ if (!waitingTraces.isEmpty()) {
+ onNewTraces(waitingTraces);
+ waitingTraces.clear();
+ } else notifyWithSaveUpScroll();
+
+ showShareContainer(false);
+ } else {
+ showShareContainer(true);
+
+ showSelectAllBetweenButton(selectedRange.last() - selectedRange.first() >= selectedRange.size());
+ }
+ }
+
+ private void notifyWithSaveUpScroll() {
+ boolean canScrollDown = rvLog.canScrollVertically(1);
+ boolean canScrollUp = rvLog.canScrollVertically(-1);
+
+ int position = linearLayoutManager.findFirstVisibleItemPosition();
+ Trace firstTrace = logRvAdapter.getItem(position);
+
+ View startView = rvLog.getChildAt(0);
+ int offset = (startView == null) ? 0 : (startView.getTop());
+ logRvAdapter.notifyDataSetChanged();
+
+ updateScrollPosition(canScrollDown, canScrollUp, offset, firstTrace);
+ }
+
+ private void updateScrollPosition(boolean canScrollDown, boolean canScrollUp, int offset, Trace firstTrace) {
+ int scrollPosition;
+ if (canScrollDown) {
+ scrollPosition = !canScrollUp ? 0 : logRvAdapter.getPosition(firstTrace);
+ } else {
+ scrollPosition = logRvAdapter.getItemCount() - 1;
+ rvLog.scrollToPosition(scrollPosition);
+ return;
+ }
+
+ if (scrollPosition == -1) scrollPosition = 0;
+
+ linearLayoutManager.scrollToPositionWithOffset(scrollPosition, scrollPosition == 0 ? 0 : offset);
+ }
+
+ private void setListWidth(boolean wrapContent, boolean notify) {
+ int displayWidth = Resources.getSystem().getDisplayMetrics().widthPixels;
+
+ if (!wrapContent) {
+ final int orientation = Resources.getSystem().getConfiguration().orientation;
+ displayWidth = orientation == Configuration.ORIENTATION_LANDSCAPE ? displayWidth * 3 : displayWidth * 6;
+ }
+ logRvAdapter.setWidth(displayWidth);
+ if (notify) {
+ notifyWithSaveUpScroll();
+ }
+ }
+
+ private void filter() {
+ boolean canScrollDown = rvLog.canScrollVertically(1);
+ boolean canScrollUp = rvLog.canScrollVertically(-1);
+
+ int position = linearLayoutManager.findFirstVisibleItemPosition();
+ Trace firstTrace = logRvAdapter.getItem(position);
+
+ View startView = rvLog.getChildAt(0);
+ int offset = (startView == null) ? 0 : (startView.getTop());
+
+ filter(etxtFilter.getText().toString(), cbRegex.isChecked(), firstTrace, offset, canScrollDown, canScrollUp);
+ }
+
+ private void filter(String searchText, boolean regex,
+ @Nullable Trace firstTrace, int offset, boolean canScrollBottom, boolean canScrollTop
+ ) {
+ if (searchText != null) searchText = searchText.toLowerCase();
+
+ if (traceLevel == TraceLevel.VERBOSE && TextUtils.isEmpty(searchText)) {
+ logRvAdapter.setNewData(traceBuffer.getTraces());
+
+ updateScrollPosition(canScrollBottom, canScrollTop, offset, firstTrace);
+ return;
+ }
+
+ List traces = new ArrayList<>();
+ Pattern pattern = null;
+ if (regex && !TextUtils.isEmpty(searchText)) try {
+ pattern = Pattern.compile(searchText);
+ } catch (PatternSyntaxException patternSyntaxException) {
+ regex = false;
+ }
+
+ for (Trace trace : traceBuffer.getTraces()) {
+ String tag = trace.tag.toLowerCase();
+ String message = trace.message.toLowerCase();
+ boolean add = traceLevel == TraceLevel.VERBOSE || trace.level.ordinal() >= traceLevel.ordinal();
+
+ if (add && !TextUtils.isEmpty(searchText)) {
+ if (regex) {
+ //noinspection ConstantConditions
+ Matcher m = pattern.matcher(tag);
+
+ add = m.find();
+ if (!add) {
+ m = pattern.matcher(message);
+ add = m.find();
+ }
+ } else {
+ add = tag.contains(searchText) || message.contains(searchText);
+ }
+ }
+ if (add) traces.add(trace);
+ }
+ logRvAdapter.setNewData(traces);
+
+ updateScrollPosition(canScrollBottom, canScrollTop, offset, firstTrace);
+ }
+}
diff --git a/logcat/src/main/java/com/devlight/logcat/adapter/LogRvAdapter.java b/logcat/src/main/java/com/devlight/logcat/adapter/LogRvAdapter.java
new file mode 100644
index 0000000..61ac5ae
--- /dev/null
+++ b/logcat/src/main/java/com/devlight/logcat/adapter/LogRvAdapter.java
@@ -0,0 +1,191 @@
+package com.devlight.logcat.adapter;
+
+import android.graphics.Color;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.core.content.ContextCompat;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.devlight.logcat.R;
+import com.devlight.logcat.model.Trace;
+import com.devlight.logcat.model.TraceLevel;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.TreeSet;
+
+/**
+ * @author Taras Yurkiv
+ * Email TYurkiv1995@gmail.com
+ * @since 19.07.2019
+ */
+public class LogRvAdapter extends RecyclerView.Adapter implements
+ View.OnClickListener {
+
+ private final OnSelectedTraceRangeChangeListener rangeChangeListener;
+ private boolean showTag;
+ private boolean showTime;
+ private int width;
+
+ private List traces;
+ private int defLogColor;
+ private int errorLogColor;
+
+ private int selectedColor;
+
+ private TreeSet selectedItems = new TreeSet<>();
+
+ public LogRvAdapter(boolean showTag, boolean showTime,
+ OnSelectedTraceRangeChangeListener rangeChangeListener) {
+ traces = new ArrayList<>();
+ this.showTag = showTag;
+ this.showTime = showTime;
+ this.rangeChangeListener = rangeChangeListener;
+ }
+
+ @NonNull
+ @Override
+ public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+ View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_log, parent, false);
+ return new ViewHolder(v);
+ }
+
+ @Override
+ public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
+ holder.itemView.setBackgroundColor(selectedItems.contains(position) ? selectedColor : Color.TRANSPARENT);
+
+ Trace trace = traces.get(position);
+ holder.txtTraceLevel.setText(trace.level.getValue());
+
+ int color = trace.level == TraceLevel.ASSERT || trace.level == TraceLevel.ERROR ? errorLogColor : defLogColor;
+ holder.txtTraceLevel.setTextColor(color);
+ holder.txtMessage.setTextColor(color);
+
+ String log = "";
+ if (showTime) log = trace.time + " ";
+ if (showTag) {
+ log += trace.tag + ": ";
+ }
+ log += trace.message;
+
+ FrameLayout.LayoutParams layoutParams = (FrameLayout.LayoutParams) holder.txtMessage.getLayoutParams();
+ layoutParams.width = width;
+ holder.txtMessage.requestLayout();
+ holder.txtMessage.setText(log);
+
+ holder.itemView.setTag(position);
+ holder.itemView.setOnClickListener(this);
+ }
+
+ @Override
+ public void onAttachedToRecyclerView(RecyclerView recyclerView) {
+ defLogColor = ContextCompat.getColor(recyclerView.getContext(), R.color.dark);
+ errorLogColor = ContextCompat.getColor(recyclerView.getContext(), R.color.log_error_color);
+ selectedColor = ContextCompat.getColor(recyclerView.getContext(), R.color.selected_log_color);
+
+ recyclerView.setItemAnimator(null);
+ super.onAttachedToRecyclerView(recyclerView);
+ }
+
+ @Override
+ public int getItemCount() {
+ return traces.size();
+ }
+
+ public void setNewData(List newData) {
+ this.traces = newData == null ? new ArrayList() : newData;
+
+ notifyDataSetChanged();
+ }
+
+ @Nullable
+ public Trace getItem(int position) {
+ if (position >= 0 && position < traces.size()) {
+ return traces.get(position);
+ }
+ return null;
+ }
+
+ public int getPosition(Trace trace) {
+ return traces.indexOf(trace);
+ }
+
+ public void setShowTag(boolean showTag) {
+ this.showTag = showTag;
+ }
+
+ public void setShowTime(boolean showTime) {
+ this.showTime = showTime;
+ }
+
+ public void setWidth(int width) {
+ this.width = width;
+ }
+
+ public void clearSelectedItems(boolean notifyCallback) {
+ selectedItems.clear();
+ if (notifyCallback) rangeChangeListener.onSelectedTracesRangeChanged(selectedItems);
+ }
+
+ public void selectAllBetween(boolean notifyCallback) {
+ for (int i = selectedItems.first(); i < selectedItems.last(); i++) {
+ selectedItems.add(i);
+ }
+ if (notifyCallback) rangeChangeListener.onSelectedTracesRangeChanged(selectedItems);
+ }
+
+ public String getSelectedTracesAsString() {
+ StringBuilder stringBuilder = new StringBuilder();
+
+ int lastPosition = selectedItems.last();
+ for (Integer selectedItem : selectedItems) {
+ Trace trace = getItem(selectedItem);
+ if (trace != null) {
+ stringBuilder.append(trace.level.getValue())
+ .append(' ')
+ .append(trace.time)
+ .append(' ')
+ .append(trace.tag)
+ .append(": ")
+ .append(trace.message);
+ if (lastPosition != selectedItem) stringBuilder.append("\n");
+ }
+ }
+ return stringBuilder.toString();
+ }
+
+ @Override
+ public void onClick(View v) {
+ int position = (int) v.getTag();
+
+ if (selectedItems.contains(position)) {
+ selectedItems.remove(position);
+ v.setBackgroundColor(Color.TRANSPARENT);
+ } else {
+ selectedItems.add(position);
+ v.setBackgroundColor(selectedColor);
+ }
+ rangeChangeListener.onSelectedTracesRangeChanged(selectedItems);
+ }
+
+ static class ViewHolder extends RecyclerView.ViewHolder {
+ final TextView txtTraceLevel;
+ final TextView txtMessage;
+
+ ViewHolder(View itemView) {
+ super(itemView);
+ txtTraceLevel = itemView.findViewById(R.id.txt_log_level);
+ txtMessage = itemView.findViewById(R.id.txt_log_text);
+ }
+ }
+
+ public interface OnSelectedTraceRangeChangeListener {
+ void onSelectedTracesRangeChanged(TreeSet selectedRange);
+ }
+}
diff --git a/logcat/src/main/java/com/devlight/logcat/exception/IllegalTraceException.java b/logcat/src/main/java/com/devlight/logcat/exception/IllegalTraceException.java
new file mode 100644
index 0000000..f8a6768
--- /dev/null
+++ b/logcat/src/main/java/com/devlight/logcat/exception/IllegalTraceException.java
@@ -0,0 +1,8 @@
+package com.devlight.logcat.exception;
+
+public class IllegalTraceException extends Exception {
+
+ public IllegalTraceException(String message) {
+ super(message);
+ }
+}
diff --git a/logcat/src/main/java/com/devlight/logcat/model/LogThread.java b/logcat/src/main/java/com/devlight/logcat/model/LogThread.java
new file mode 100644
index 0000000..d0bc97f
--- /dev/null
+++ b/logcat/src/main/java/com/devlight/logcat/model/LogThread.java
@@ -0,0 +1,77 @@
+package com.devlight.logcat.model;
+
+import android.util.Log;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+
+class LogThread extends Thread implements Cloneable {
+ private static final String LOGTAG = "LogThread";
+
+ private Process process;
+ private BufferedReader bufferReader;
+ private TraceListener listener;
+ private boolean continueReading = true;
+
+ public void setListener(TraceListener listener) {
+ this.listener = listener;
+ }
+
+ public TraceListener getListener() {
+ return listener;
+ }
+
+ @Override
+ public void run() {
+ super.run();
+ try {
+ process = Runtime.getRuntime().exec("logcat -v time");
+ } catch (IOException e) {
+ Log.e(LOGTAG, "IOException executing logcat command.", e);
+ }
+ readLogcat();
+ }
+
+ public void stopReading() {
+ continueReading = false;
+ }
+
+ private void readLogcat() {
+ BufferedReader bufferedReader = getBufferReader();
+ try {
+ String trace = bufferedReader.readLine();
+ while (trace != null && continueReading) {
+ notifyListener(trace);
+ trace = bufferedReader.readLine();
+ }
+ } catch (IOException e) {
+ Log.e(LOGTAG, "IOException reading logcat trace.", e);
+ }
+ }
+
+ private void notifyListener(String trace) {
+ if (listener != null) {
+ listener.onNewTraceRead(trace);
+ }
+ }
+
+ private BufferedReader getBufferReader() {
+ if (bufferReader == null) {
+ bufferReader = new BufferedReader(new InputStreamReader(process.getInputStream()));
+ }
+ return bufferReader;
+ }
+
+ @Override
+ public Object clone() {
+ LogThread logcat = new LogThread();
+ logcat.setListener(listener);
+ return logcat;
+ }
+
+ interface TraceListener {
+
+ void onNewTraceRead(String trace);
+ }
+}
diff --git a/logcat/src/main/java/com/devlight/logcat/model/Logcat.java b/logcat/src/main/java/com/devlight/logcat/model/Logcat.java
new file mode 100644
index 0000000..d033d0f
--- /dev/null
+++ b/logcat/src/main/java/com/devlight/logcat/model/Logcat.java
@@ -0,0 +1,107 @@
+package com.devlight.logcat.model;
+
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+
+import com.devlight.logcat.exception.IllegalTraceException;
+
+import java.util.LinkedList;
+import java.util.List;
+
+public class Logcat {
+
+ private final int TIME_BETWEEN_TRACE_CONSTANT = 1;
+ private final int MAX_DELAY_TIME_CONSTANT = 2;
+ private final long TIME_BETWEEN_TRACE = 700;
+ private final long MAX_DELAY_TIME = 5000;
+
+ private LogThread logcat;
+ private final Handler mainThreadHandler = new Handler(Looper.getMainLooper()) {
+ @Override
+ public void handleMessage(Message msg) {
+ forceNotifyNewTraces();
+ }
+ };
+ private final List tracesToNotify;
+ private Listener listener;
+
+ public Logcat() {
+ this.tracesToNotify = new LinkedList<>();
+ this.logcat = new LogThread();
+ }
+
+ public void startReading() {
+ logcat.setListener(new LogThread.TraceListener() {
+ @Override
+ public void onNewTraceRead(String trace) {
+ try {
+ Logcat.this.addTraceToTheBuffer(trace);
+ } catch (IllegalTraceException e) {
+ return;
+ }
+ Logcat.this.notifyNewTraces();
+ }
+ });
+ boolean logcatWasNotStarted = Thread.State.NEW.equals(logcat.getState());
+ if (logcatWasNotStarted) {
+ logcat.start();
+ } else restart();
+ }
+
+ public void stopReading() {
+ logcat.stopReading();
+ logcat.interrupt();
+ }
+
+ public void restart() {
+ logcat.stopReading();
+ logcat.interrupt();
+ logcat = (LogThread) logcat.clone();
+ tracesToNotify.clear();
+ mainThreadHandler.removeMessages(TIME_BETWEEN_TRACE_CONSTANT);
+ mainThreadHandler.removeMessages(MAX_DELAY_TIME_CONSTANT);
+ logcat.start();
+ }
+
+ public synchronized void setListener(Logcat.Listener listener) {
+ this.listener = listener;
+ }
+
+ private synchronized void addTraceToTheBuffer(String logcatTrace) throws IllegalTraceException {
+ Trace trace = Trace.fromString(logcatTrace);
+ tracesToNotify.add(trace);
+ }
+
+ private void notifyNewTraces() {
+ if (!mainThreadHandler.hasMessages(MAX_DELAY_TIME_CONSTANT)) {
+ mainThreadHandler.sendEmptyMessageDelayed(MAX_DELAY_TIME_CONSTANT, MAX_DELAY_TIME);
+ }
+ mainThreadHandler.removeMessages(TIME_BETWEEN_TRACE_CONSTANT);
+ mainThreadHandler.sendEmptyMessageDelayed(TIME_BETWEEN_TRACE_CONSTANT, TIME_BETWEEN_TRACE);
+ }
+
+ private void forceNotifyNewTraces() {
+ mainThreadHandler.removeMessages(MAX_DELAY_TIME_CONSTANT);
+ mainThreadHandler.sendEmptyMessageDelayed(MAX_DELAY_TIME_CONSTANT, MAX_DELAY_TIME);
+
+ if (tracesToNotify.size() > 0) {
+ final List traces = new LinkedList<>(tracesToNotify);
+ tracesToNotify.clear();
+ notifyListeners(traces);
+ }
+ }
+
+ private synchronized void notifyListeners(final List traces) {
+ mainThreadHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ if (listener != null) listener.onNewTraces(traces);
+ }
+ });
+ }
+
+ public interface Listener {
+ void onNewTraces(List traces);
+ }
+}
diff --git a/logcat/src/main/java/com/devlight/logcat/model/Trace.java b/logcat/src/main/java/com/devlight/logcat/model/Trace.java
new file mode 100644
index 0000000..2883d10
--- /dev/null
+++ b/logcat/src/main/java/com/devlight/logcat/model/Trace.java
@@ -0,0 +1,68 @@
+package com.devlight.logcat.model;
+
+
+import com.devlight.logcat.exception.IllegalTraceException;
+
+public class Trace {
+ private static final String TAG = "TraceLOG";
+ private static final char TRACE_LEVEL_SEPARATOR = '/';
+ private static final int START_OF_DATE_INDEX = 6;
+ private static final int END_OF_DATE_INDEX = 18;
+ private static final int START_OF_MESSAGE_INDEX = 21;
+ private static final int MIN_TRACE_SIZE = 21;
+ static final int TRACE_LEVEL_INDEX = 19;
+
+ public final TraceLevel level;
+ public final String time;
+ public final String tag;
+ public final String message;
+
+ public Trace(TraceLevel level, String time, String tag, String message) {
+ this.level = level;
+ this.time = time;
+ this.tag = tag;
+ this.message = message;
+ }
+
+ public static Trace fromString(String logcatTrace) throws IllegalTraceException {
+ if (logcatTrace == null
+ || logcatTrace.length() < MIN_TRACE_SIZE
+ || logcatTrace.charAt(20) != TRACE_LEVEL_SEPARATOR
+ || logcatTrace.contains(TAG)) {
+ throw new IllegalTraceException(
+ "You are trying to create a Trace object from a invalid String. Your trace have to be "
+ + "something like: '07-19 15:24:11.162 D/TAG(30345): Some message'.");
+ }
+ TraceLevel level = TraceLevel.getTraceLevel(logcatTrace.charAt(TRACE_LEVEL_INDEX));
+ String time = logcatTrace.substring(START_OF_DATE_INDEX, END_OF_DATE_INDEX);
+ String message = logcatTrace.substring(START_OF_MESSAGE_INDEX);
+
+ String tag = message.substring(0, message.indexOf(':'));
+ tag = tag.replaceFirst(" *\\(.+\\)", "");
+ message = message.substring(message.indexOf(':') + 2);
+
+ return new Trace(level, time, tag, message);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ Trace trace = (Trace) o;
+
+ if (level != trace.level) return false;
+ if (time != null ? !time.equals(trace.time) : trace.time != null) return false;
+ if (tag != null ? !tag.equals(trace.tag) : trace.tag != null) return false;
+ return message != null ? message.equals(trace.message) : trace.message == null;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = level != null ? level.hashCode() : 0;
+ result = 31 * result + (time != null ? time.hashCode() : 0);
+ result = 31 * result + (tag != null ? tag.hashCode() : 0);
+ result = 31 * result + (message != null ? message.hashCode() : 0);
+ return result;
+ }
+}
\ No newline at end of file
diff --git a/logcat/src/main/java/com/devlight/logcat/model/TraceBuffer.java b/logcat/src/main/java/com/devlight/logcat/model/TraceBuffer.java
new file mode 100644
index 0000000..6fb0e89
--- /dev/null
+++ b/logcat/src/main/java/com/devlight/logcat/model/TraceBuffer.java
@@ -0,0 +1,54 @@
+package com.devlight.logcat.model;
+
+import java.util.LinkedList;
+import java.util.List;
+
+public class TraceBuffer {
+
+ private int bufferSize;
+ private final List traces;
+
+ public TraceBuffer(int bufferSize) {
+ this.bufferSize = bufferSize;
+ traces = new LinkedList<>();
+ }
+
+ public void setBufferSize(int bufferSize) {
+ this.bufferSize = bufferSize;
+ removeExceededTracesIfNeeded();
+ }
+
+ public int add(List traces) {
+ this.traces.addAll(traces);
+ return removeExceededTracesIfNeeded();
+ }
+
+ public List getTraces() {
+ return traces;
+ }
+
+ public void clear() {
+ traces.clear();
+ }
+
+ private int removeExceededTracesIfNeeded() {
+ int tracesToDiscard = getNumberOfTracesToDiscard();
+ if (tracesToDiscard > 0) {
+ discardTraces(tracesToDiscard);
+ }
+ return tracesToDiscard;
+ }
+
+ private int getNumberOfTracesToDiscard() {
+ int currentTracesSize = this.traces.size();
+ int tracesToDiscard = currentTracesSize - bufferSize;
+ tracesToDiscard = tracesToDiscard < 0 ? 0 : tracesToDiscard;
+ return tracesToDiscard;
+ }
+
+ private void discardTraces(int tracesToDiscard) {
+ if (tracesToDiscard > 0) {
+ traces.subList(0, tracesToDiscard).clear();
+ }
+ }
+}
diff --git a/logcat/src/main/java/com/devlight/logcat/model/TraceLevel.java b/logcat/src/main/java/com/devlight/logcat/model/TraceLevel.java
new file mode 100644
index 0000000..d038115
--- /dev/null
+++ b/logcat/src/main/java/com/devlight/logcat/model/TraceLevel.java
@@ -0,0 +1,59 @@
+package com.devlight.logcat.model;
+
+/**
+ * Logcat trace levels used to indicate the trace importance.
+ */
+public enum TraceLevel {
+ VERBOSE("V"), DEBUG("D"), INFO("I"), WARN("W"), ERROR("E"), ASSERT("A"), WTF("F");
+
+ private final String value;
+
+ TraceLevel(String value) {
+ this.value = value;
+ }
+
+ public String getValue() {
+ return value;
+ }
+
+ public String getName() {
+ String name = name().toLowerCase();
+ name = name.substring(0, 1).toUpperCase() + name.substring(1);
+ return name;
+ }
+
+ public static TraceLevel getTraceLevel(char trace) {
+ TraceLevel traceLevel;
+ switch (trace) {
+ case 'V':
+ traceLevel = VERBOSE;
+ break;
+ case 'A':
+ traceLevel = ASSERT;
+ break;
+ case 'I':
+ traceLevel = INFO;
+ break;
+ case 'W':
+ traceLevel = WARN;
+ break;
+ case 'E':
+ traceLevel = ERROR;
+ break;
+ case 'F':
+ traceLevel = WTF;
+ break;
+ default:
+ traceLevel = DEBUG;
+ }
+ return traceLevel;
+ }
+
+ public static TraceLevel getTraceLevel(String trace) {
+ trace = trace.toUpperCase();
+ for (TraceLevel value : TraceLevel.values()) {
+ if (value.name().equals(trace)) return value;
+ }
+ return TraceLevel.DEBUG;
+ }
+}
diff --git a/logcat/src/main/res/drawable-v21/bg_btn_floating.xml b/logcat/src/main/res/drawable-v21/bg_btn_floating.xml
new file mode 100644
index 0000000..4c02273
--- /dev/null
+++ b/logcat/src/main/res/drawable-v21/bg_btn_floating.xml
@@ -0,0 +1,16 @@
+
+
+
+ -
+
+
+
+
+
+ -
+
+
+
+
+
\ No newline at end of file
diff --git a/logcat/src/main/res/drawable-v21/bg_icon_ripple.xml b/logcat/src/main/res/drawable-v21/bg_icon_ripple.xml
new file mode 100644
index 0000000..976bf6d
--- /dev/null
+++ b/logcat/src/main/res/drawable-v21/bg_icon_ripple.xml
@@ -0,0 +1,4 @@
+
+
\ No newline at end of file
diff --git a/logcat/src/main/res/drawable/bg_btn_floating.xml b/logcat/src/main/res/drawable/bg_btn_floating.xml
new file mode 100644
index 0000000..0614e58
--- /dev/null
+++ b/logcat/src/main/res/drawable/bg_btn_floating.xml
@@ -0,0 +1,15 @@
+
+
+
+ -
+
+
+
+
+
+ -
+
+
+
+
+
diff --git a/logcat/src/main/res/drawable/bg_icon_ripple.xml b/logcat/src/main/res/drawable/bg_icon_ripple.xml
new file mode 100644
index 0000000..8b0d97f
--- /dev/null
+++ b/logcat/src/main/res/drawable/bg_icon_ripple.xml
@@ -0,0 +1,10 @@
+
+
+
+ -
+
+
+
+
+
+
\ No newline at end of file
diff --git a/logcat/src/main/res/drawable/bg_search.xml b/logcat/src/main/res/drawable/bg_search.xml
new file mode 100644
index 0000000..0d6056e
--- /dev/null
+++ b/logcat/src/main/res/drawable/bg_search.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/logcat/src/main/res/drawable/ic_arrow_down.xml b/logcat/src/main/res/drawable/ic_arrow_down.xml
new file mode 100644
index 0000000..22c401f
--- /dev/null
+++ b/logcat/src/main/res/drawable/ic_arrow_down.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/logcat/src/main/res/drawable/ic_bar_search_gray.xml b/logcat/src/main/res/drawable/ic_bar_search_gray.xml
new file mode 100644
index 0000000..a52c115
--- /dev/null
+++ b/logcat/src/main/res/drawable/ic_bar_search_gray.xml
@@ -0,0 +1,12 @@
+
+
+
diff --git a/logcat/src/main/res/drawable/ic_clear_search_field.xml b/logcat/src/main/res/drawable/ic_clear_search_field.xml
new file mode 100644
index 0000000..130c0f2
--- /dev/null
+++ b/logcat/src/main/res/drawable/ic_clear_search_field.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/logcat/src/main/res/drawable/ic_close_black.xml b/logcat/src/main/res/drawable/ic_close_black.xml
new file mode 100644
index 0000000..23c38fb
--- /dev/null
+++ b/logcat/src/main/res/drawable/ic_close_black.xml
@@ -0,0 +1,12 @@
+
+
+
diff --git a/logcat/src/main/res/drawable/ic_content_copy.xml b/logcat/src/main/res/drawable/ic_content_copy.xml
new file mode 100644
index 0000000..8a894a3
--- /dev/null
+++ b/logcat/src/main/res/drawable/ic_content_copy.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/logcat/src/main/res/drawable/ic_format_indent_increase.xml b/logcat/src/main/res/drawable/ic_format_indent_increase.xml
new file mode 100644
index 0000000..d8a6b90
--- /dev/null
+++ b/logcat/src/main/res/drawable/ic_format_indent_increase.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/logcat/src/main/res/drawable/ic_gray_arrow_down.xml b/logcat/src/main/res/drawable/ic_gray_arrow_down.xml
new file mode 100644
index 0000000..adf7f19
--- /dev/null
+++ b/logcat/src/main/res/drawable/ic_gray_arrow_down.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/logcat/src/main/res/drawable/ic_share.xml b/logcat/src/main/res/drawable/ic_share.xml
new file mode 100644
index 0000000..e3fe874
--- /dev/null
+++ b/logcat/src/main/res/drawable/ic_share.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/logcat/src/main/res/layout/activity_logcat.xml b/logcat/src/main/res/layout/activity_logcat.xml
new file mode 100644
index 0000000..5accf4f
--- /dev/null
+++ b/logcat/src/main/res/layout/activity_logcat.xml
@@ -0,0 +1,223 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/logcat/src/main/res/layout/item_log.xml b/logcat/src/main/res/layout/item_log.xml
new file mode 100644
index 0000000..b6f6581
--- /dev/null
+++ b/logcat/src/main/res/layout/item_log.xml
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
diff --git a/logcat/src/main/res/menu/popup_trace_level.xml b/logcat/src/main/res/menu/popup_trace_level.xml
new file mode 100644
index 0000000..2a9aedd
--- /dev/null
+++ b/logcat/src/main/res/menu/popup_trace_level.xml
@@ -0,0 +1,28 @@
+
+
\ No newline at end of file
diff --git a/logcat/src/main/res/values-v21/styles.xml b/logcat/src/main/res/values-v21/styles.xml
new file mode 100644
index 0000000..6a79de7
--- /dev/null
+++ b/logcat/src/main/res/values-v21/styles.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/logcat/src/main/res/values-v23/styles.xml b/logcat/src/main/res/values-v23/styles.xml
new file mode 100644
index 0000000..2878bda
--- /dev/null
+++ b/logcat/src/main/res/values-v23/styles.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/logcat/src/main/res/values-v27/styles.xml b/logcat/src/main/res/values-v27/styles.xml
new file mode 100644
index 0000000..361b8e7
--- /dev/null
+++ b/logcat/src/main/res/values-v27/styles.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/logcat/src/main/res/values/colors.xml b/logcat/src/main/res/values/colors.xml
new file mode 100644
index 0000000..ab7bbde
--- /dev/null
+++ b/logcat/src/main/res/values/colors.xml
@@ -0,0 +1,15 @@
+
+
+ #14000000
+
+ #C20009
+ #8A5C9ABB
+
+ #ffffff
+ #282b3c
+ #454f5b
+ #919eab
+
+ #bddbdbdb
+ #dadada
+
\ No newline at end of file
diff --git a/logcat/src/main/res/values/dimens.xml b/logcat/src/main/res/values/dimens.xml
new file mode 100644
index 0000000..36a778f
--- /dev/null
+++ b/logcat/src/main/res/values/dimens.xml
@@ -0,0 +1,16 @@
+
+
+ 4dp
+ 24dp
+
+ 60dp
+ 56dp
+
+ 10sp
+ 14sp
+ 4dp
+ 8dp
+ 12dp
+ 16dp
+
+
\ No newline at end of file
diff --git a/logcat/src/main/res/values/strings.xml b/logcat/src/main/res/values/strings.xml
new file mode 100644
index 0000000..ba1da3b
--- /dev/null
+++ b/logcat/src/main/res/values/strings.xml
@@ -0,0 +1,9 @@
+
+ Logcat
+ Filter
+ Regex
+ Show time
+ Show TAG
+ Wrap log
+ Log copied
+
diff --git a/logcat/src/main/res/values/styles.xml b/logcat/src/main/res/values/styles.xml
new file mode 100644
index 0000000..44fb868
--- /dev/null
+++ b/logcat/src/main/res/values/styles.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
diff --git a/settings.gradle b/settings.gradle
new file mode 100644
index 0000000..872d485
--- /dev/null
+++ b/settings.gradle
@@ -0,0 +1 @@
+include ':app', ':logcat'