diff --git a/.gradle/6.7.1/fileChanges/last-build.bin b/.gradle/6.7.1/fileChanges/last-build.bin new file mode 100644 index 0000000..f76dd23 Binary files /dev/null and b/.gradle/6.7.1/fileChanges/last-build.bin differ diff --git a/.gradle/6.7.1/fileHashes/fileHashes.bin b/.gradle/6.7.1/fileHashes/fileHashes.bin new file mode 100644 index 0000000..5cbd816 Binary files /dev/null and b/.gradle/6.7.1/fileHashes/fileHashes.bin differ diff --git a/.gradle/6.7.1/fileHashes/fileHashes.lock b/.gradle/6.7.1/fileHashes/fileHashes.lock new file mode 100644 index 0000000..14bd27d Binary files /dev/null and b/.gradle/6.7.1/fileHashes/fileHashes.lock differ diff --git a/.gradle/6.7.1/gc.properties b/.gradle/6.7.1/gc.properties new file mode 100644 index 0000000..e69de29 diff --git a/.gradle/buildOutputCleanup/buildOutputCleanup.lock b/.gradle/buildOutputCleanup/buildOutputCleanup.lock new file mode 100644 index 0000000..5280406 Binary files /dev/null and b/.gradle/buildOutputCleanup/buildOutputCleanup.lock differ diff --git a/.gradle/buildOutputCleanup/cache.properties b/.gradle/buildOutputCleanup/cache.properties new file mode 100644 index 0000000..5efa841 --- /dev/null +++ b/.gradle/buildOutputCleanup/cache.properties @@ -0,0 +1,2 @@ +#Wed Aug 25 17:16:23 IST 2021 +gradle.version=6.7.1 diff --git a/.gradle/checksums/checksums.lock b/.gradle/checksums/checksums.lock new file mode 100644 index 0000000..6547bb5 Binary files /dev/null and b/.gradle/checksums/checksums.lock differ diff --git a/.gradle/configuration-cache/gc.properties b/.gradle/configuration-cache/gc.properties new file mode 100644 index 0000000..e69de29 diff --git a/.gradle/vcs-1/gc.properties b/.gradle/vcs-1/gc.properties new file mode 100644 index 0000000..e69de29 diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..26d3352 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/.idea/.name b/.idea/.name new file mode 100644 index 0000000..21966d7 --- /dev/null +++ b/.idea/.name @@ -0,0 +1 @@ +Snippet \ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 0000000..61a9130 --- /dev/null +++ b/.idea/compiler.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..1bca317 --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,20 @@ + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..d5d35ec --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,9 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..8faac21 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml new file mode 100644 index 0000000..797acea --- /dev/null +++ b/.idea/runConfigurations.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /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..42afabf --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..8847803 --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,42 @@ +plugins { + id 'com.android.application' +} + +android { + compileSdkVersion 30 + buildToolsVersion "29.0.3" + + defaultConfig { + applicationId "com.microsoft.snippet" + minSdkVersion 19 + //noinspection OldTargetApi + targetSdkVersion 29 + versionCode 1 + versionName "1.0" + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } +} + +dependencies { + + implementation 'androidx.appcompat:appcompat:1.3.1' + implementation 'com.google.android.material:material:1.4.0' + implementation 'androidx.constraintlayout:constraintlayout:2.1.0' + implementation project(':snippet') + + testImplementation 'junit:junit:4.+' + androidTestImplementation 'androidx.test.ext:junit:1.1.3' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' +} \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /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 \ No newline at end of file diff --git a/app/src/androidTest/java/com/microsoft/snippet/ExampleInstrumentedTest.java b/app/src/androidTest/java/com/microsoft/snippet/ExampleInstrumentedTest.java new file mode 100644 index 0000000..38ec9dc --- /dev/null +++ b/app/src/androidTest/java/com/microsoft/snippet/ExampleInstrumentedTest.java @@ -0,0 +1,26 @@ +package com.microsoft.snippet; + +import android.content.Context; + +import androidx.test.platform.app.InstrumentationRegistry; +import androidx.test.ext.junit.runners.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() { + // Context of the app under test. + Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); + assertEquals("com.microsoft.snippet", appContext.getPackageName()); + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..8cca835 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/microsoft/snippet/MainActivity.java b/app/src/main/java/com/microsoft/snippet/MainActivity.java new file mode 100644 index 0000000..7ccbf1d --- /dev/null +++ b/app/src/main/java/com/microsoft/snippet/MainActivity.java @@ -0,0 +1,18 @@ +package com.microsoft.snippet; + +import androidx.appcompat.app.AppCompatActivity; + +import android.os.Bundle; + +public class MainActivity extends AppCompatActivity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + Snippet.install(new Snippet.MeasuredExecutionPath()); + Snippet.capture(() -> { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + }); + + } +} \ 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..2b068d1 --- /dev/null +++ b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file 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..07d5da9 --- /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..4fc2444 --- /dev/null +++ b/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,18 @@ + + + + + + \ 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..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..a571e60 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..61da551 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..c41dd28 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..db5080a 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..6dba46d 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..da31a87 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..15ac681 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..b216f2d 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..f25a419 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..e96783c 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-night/themes.xml b/app/src/main/res/values-night/themes.xml new file mode 100644 index 0000000..9bb62c2 --- /dev/null +++ b/app/src/main/res/values-night/themes.xml @@ -0,0 +1,16 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..f8c6127 --- /dev/null +++ b/app/src/main/res/values/colors.xml @@ -0,0 +1,10 @@ + + + #FFBB86FC + #FF6200EE + #FF3700B3 + #FF03DAC5 + #FF018786 + #FF000000 + #FFFFFFFF + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..96329db --- /dev/null +++ b/app/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + Snippet + \ No newline at end of file diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml new file mode 100644 index 0000000..1a35b2b --- /dev/null +++ b/app/src/main/res/values/themes.xml @@ -0,0 +1,16 @@ + + + + \ No newline at end of file diff --git a/app/src/test/java/com/microsoft/snippet/ExampleUnitTest.java b/app/src/test/java/com/microsoft/snippet/ExampleUnitTest.java new file mode 100644 index 0000000..9fcd989 --- /dev/null +++ b/app/src/test/java/com/microsoft/snippet/ExampleUnitTest.java @@ -0,0 +1,17 @@ +package com.microsoft.snippet; + +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() { + assertEquals(4, 2 + 2); + } +} \ No newline at end of file diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..79c656c --- /dev/null +++ b/build.gradle @@ -0,0 +1,25 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. +buildscript { + repositories { + google() + mavenCentral() + } + dependencies { + classpath 'com.android.tools.build:gradle:4.2.2' + + // NOTE: Do not place your application dependencies here; they belong + // in the individual module build.gradle files + } +} + +allprojects { + repositories { + google() + mavenCentral() + jcenter() // Warning: this repository is going to shut down soon + } +} + +task clean(type: Delete) { + delete rootProject.buildDir +} \ No newline at end of file diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..6826e61 --- /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=-Xmx2048m -Dfile.encoding=UTF-8 +# 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 \ No newline at end of file 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..74a7154 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Wed Aug 25 17:13:35 IST 2021 +distributionBase=GRADLE_USER_HOME +distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-bin.zip +distributionPath=wrapper/dists +zipStorePath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME 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..e95643d --- /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/local.properties b/local.properties new file mode 100644 index 0000000..1820bc8 --- /dev/null +++ b/local.properties @@ -0,0 +1,10 @@ +## This file is automatically generated by Android Studio. +# Do not modify this file -- YOUR CHANGES WILL BE ERASED! +# +# This file should *NOT* be checked into Version Control Systems, +# as it contains information specific to your local configuration. +# +# Location of the SDK. This is only used by Gradle. +# For customization when using a Version Control System, please read the +# header note. +sdk.dir=/Users/vishalratna/Library/Android/sdk \ No newline at end of file diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..389f10c --- /dev/null +++ b/settings.gradle @@ -0,0 +1,3 @@ +rootProject.name = "Snippet" +include ':app' +include ':snippet' diff --git a/snippet/.gitignore b/snippet/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/snippet/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/snippet/build.gradle b/snippet/build.gradle new file mode 100644 index 0000000..c3e6bf1 --- /dev/null +++ b/snippet/build.gradle @@ -0,0 +1,38 @@ +plugins { + id 'com.android.application' +} + +android { + compileSdkVersion 30 + buildToolsVersion "29.0.3" + + defaultConfig { + applicationId "com.microsoft.snippet" + minSdkVersion 19 + targetSdkVersion 30 + versionCode 1 + versionName "1.0" + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } +} + +dependencies { + + implementation 'androidx.appcompat:appcompat:1.3.1' + implementation 'com.google.android.material:material:1.4.0' + testImplementation 'junit:junit:4.+' + androidTestImplementation 'androidx.test.ext:junit:1.1.3' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' +} \ No newline at end of file diff --git a/snippet/proguard-rules.pro b/snippet/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/snippet/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 \ No newline at end of file diff --git a/snippet/src/androidTest/java/com/microsoft/snippet/ExampleInstrumentedTest.java b/snippet/src/androidTest/java/com/microsoft/snippet/ExampleInstrumentedTest.java new file mode 100644 index 0000000..38ec9dc --- /dev/null +++ b/snippet/src/androidTest/java/com/microsoft/snippet/ExampleInstrumentedTest.java @@ -0,0 +1,26 @@ +package com.microsoft.snippet; + +import android.content.Context; + +import androidx.test.platform.app.InstrumentationRegistry; +import androidx.test.ext.junit.runners.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() { + // Context of the app under test. + Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); + assertEquals("com.microsoft.snippet", appContext.getPackageName()); + } +} \ No newline at end of file diff --git a/snippet/src/main/AndroidManifest.xml b/snippet/src/main/AndroidManifest.xml new file mode 100644 index 0000000..30a7b1c --- /dev/null +++ b/snippet/src/main/AndroidManifest.xml @@ -0,0 +1,13 @@ + + + + + + \ No newline at end of file diff --git a/snippet/src/main/java/com/microsoft/snippet/ExecutionContext.java b/snippet/src/main/java/com/microsoft/snippet/ExecutionContext.java new file mode 100644 index 0000000..19be065 --- /dev/null +++ b/snippet/src/main/java/com/microsoft/snippet/ExecutionContext.java @@ -0,0 +1,53 @@ +package com.microsoft.snippet; + +/** + * Wraps all the information that can be returned through the library. + * If any other information is needed it can be added ad-hoc. + */ +public class ExecutionContext { + private String mClass; + private String mMethod; + private int mLineNo; + private String mThreadName; + private long mExecutionDuration; + + void setClassName(String clazz) { + this.mClass = clazz; + } + + void setMethod(String method) { + this.mMethod = method; + } + + void setLineNo(int line) { + this.mLineNo = line; + } + + void setThreadName(String threadName) { + this.mThreadName = threadName; + } + + void setExecutionDuration(long duration) { + this.mExecutionDuration = duration; + } + + public String getClassName() { + return this.mClass; + } + + public String getMethodName() { + return this.mMethod; + } + + public String getThreadName() { + return this.mThreadName; + } + + public int getLineNo() { + return this.mLineNo; + } + + public long getExecutionDuration() { + return this.mExecutionDuration; + } +} diff --git a/snippet/src/main/java/com/microsoft/snippet/ExecutionPath.java b/snippet/src/main/java/com/microsoft/snippet/ExecutionPath.java new file mode 100644 index 0000000..b3122e8 --- /dev/null +++ b/snippet/src/main/java/com/microsoft/snippet/ExecutionPath.java @@ -0,0 +1,68 @@ +/* + * Copyright © Microsoft Corporation. All rights reserved. + */ + +package com.microsoft.snippet; + + +import androidx.annotation.NonNull; + +import com.microsoft.snippet.token.ILogToken; + +/** + * Execution path determines how core the functionality of this library should behave. + * It might be possible that we do not want to execute the code entirely in release builds or + * may want to add some extra information into the existing information and add it to files. + * We can plugin a custom execution path or method through this. + */ +public interface ExecutionPath { + + /** + * Return a pair of delta: the time taken to execute the closure. And the standard log message + * printed by the Snippet. + * + * @param message Custom message to print on the log if any. + * @param closure Closure who execution duration needs to be measured. + * @return Pair of Delta and Log String + */ + @NonNull + ExecutionContext capture(String message, Snippet.Closure closure); + + /** + * Return a pair of delta: the time taken to execute the closure. And the standard log message + * printed by the Snippet. + * + * @param closure Closure who execution duration needs to be measured. + * @return Pair of Delta and Log String + */ + @NonNull + ExecutionContext capture(Snippet.Closure closure); + + /** + * Returns a log token and starts the measurement at this point. The token returned has a + * method endCapture() which ends the measurement. + * + * @return IToken + */ + ILogToken startCapture(); + + /** + * Gets a log token and ties it with a tag that can be used to search the instance of log token + * using {@link Snippet#find(String)} + * + * @param tag Unique tag for the log token. If 2 token try to use same tag, the first one to get + * the tag wins and other calls becomes no-op. + * @return IToken + */ + ILogToken startCapture(String tag); + + /** + * Finds a log token with a tag which has been created previously. Using + * {@link Snippet#startCapture(String)} + * + * @param tag Custom tag. This tag will be used to search the log token across the app. + * @return IToken if any available attached with this tag + */ + ILogToken find(String tag); + +} diff --git a/snippet/src/main/java/com/microsoft/snippet/Final.java b/snippet/src/main/java/com/microsoft/snippet/Final.java new file mode 100644 index 0000000..fffd209 --- /dev/null +++ b/snippet/src/main/java/com/microsoft/snippet/Final.java @@ -0,0 +1,29 @@ +/* + * Copyright © Microsoft Corporation. All rights reserved. + */ + +package com.microsoft.snippet; + +/** + * Provides a class that can be used for capturing variables in an anonymous class implementation. + * + * @param + */ +public final class Final { + private T mValue; + + public Final() { + } + + public Final(T value) { + this.mValue = value; + } + + public T get() { + return mValue; + } + + public void set(T value) { + this.mValue = value; + } +} diff --git a/snippet/src/main/java/com/microsoft/snippet/ILogTokenSearcher.java b/snippet/src/main/java/com/microsoft/snippet/ILogTokenSearcher.java new file mode 100644 index 0000000..d79326b --- /dev/null +++ b/snippet/src/main/java/com/microsoft/snippet/ILogTokenSearcher.java @@ -0,0 +1,21 @@ +/* + * Copyright © Microsoft Corporation. All rights reserved. + */ + +package com.microsoft.snippet; + +import com.microsoft.snippet.token.ILogToken; + +/** + * Log token Searcher. Not for external use + */ +interface ILogTokenSearcher { + + /** + * Return the token for the tag provided else return null + * + * @param tag tag for the log token + * @return Log Token + */ + ILogToken search(String tag); +} diff --git a/snippet/src/main/java/com/microsoft/snippet/LogTokenPool.java b/snippet/src/main/java/com/microsoft/snippet/LogTokenPool.java new file mode 100644 index 0000000..80ed078 --- /dev/null +++ b/snippet/src/main/java/com/microsoft/snippet/LogTokenPool.java @@ -0,0 +1,119 @@ +/* + * Copyright © Microsoft Corporation. All rights reserved. + */ + +package com.microsoft.snippet; + +import android.util.Log; + +import androidx.annotation.NonNull; + +import com.microsoft.snippet.token.ILogToken; +import com.microsoft.snippet.token.LogTokenState; + +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; + +/** + * LogToken Pool is helps recycling the log token objects that are used by Snippet. + */ +public final class LogTokenPool { + private static final String TAG = LogTokenPool.class.getSimpleName(); + + private final List mPool = new LinkedList<>(); + + // A register to keep track of allocated tokens. + private final HashSet mRegister = new HashSet<>(); + + LogTokenPool() { + } + + @NonNull + ILogToken obtain() { + if (Snippet.mPrintDebugLogs) { + Log.d(TAG, "obtain() called"); + Log.d(TAG, "Number of objects in POOL while entering into obtain(): " + mPool.size()); + } + ILogToken temp; + synchronized (this) { + if (mPool.size() > 0) { + if (Snippet.mPrintDebugLogs) { + Log.d(TAG, "Pool has reusable objects available. Will use one."); + } + temp = mPool.remove(0); + boolean isAddingToRegisterSuccess = mRegister.add(temp.hashCode()); + if (isAddingToRegisterSuccess) { + temp.setState(LogTokenState.ACTIVE); + return temp; + } else { + Log.d(TAG, "Register already has the hashcode belonging to the current token, we need to recycle it and create a new one! "); + return assureTokenWithUniqueHashCode(temp); + } + } else { + if (Snippet.mPrintDebugLogs) { + Log.d(TAG, "Pool is empty, a new LogToken object will be created."); + } + temp = createTokenLocked(); + boolean isAddingToRegisterSuccess = mRegister.add(temp.hashCode()); + if (isAddingToRegisterSuccess) { + temp.setState(LogTokenState.ACTIVE); + return temp; + } else { + Log.d(TAG, "Register already has the hashcode belonging to the current token, we need to recycle it and create a new one! "); + return assureTokenWithUniqueHashCode(temp); + } + } + } + } + + /** + * This method gives a one level safety against the situation where the hash codes of 2 log tokens + * become same accidentally given the hash functions are not messed up. It recycles the old token + * that has the same hash code that exists in the register and tries to generate a new token that + * will be having different hash code. + * If someone has messed the hash function badly. This will not help. + * + * @param oldToken old token that needs to get recycled. + * @return new token + */ + @NonNull + ILogToken assureTokenWithUniqueHashCode(ILogToken oldToken) { + // Return old token to the pool + oldToken.reset(); + mPool.add(oldToken); + + // create a new token and return it. + ILogToken newToken = createTokenLocked(); + newToken.setState(LogTokenState.ACTIVE); + return newToken; + } + + synchronized void recycle(@NonNull ILogToken token) { + if (Snippet.mPrintDebugLogs) { + Log.d(TAG, "recycle() called"); + Log.d(TAG, "Number of objects in POOL while entering into recycle(): " + mPool.size()); + } + + // See if we are returning token which was not created using obtain() + if (!mRegister.contains(token.hashCode())) { + throw new IllegalStateException("Trying to return object which was not created using obtain() " + + "OR May be endCapture() was called multiple times on the same token object."); + } else { + if (Snippet.mPrintDebugLogs) { + Log.d(TAG, " Recycling the LogToken object in the pool."); + } + token.reset(); + token.setState(LogTokenState.IN_POOL); + mPool.add(token); + mRegister.remove(token.hashCode()); + if (Snippet.mPrintDebugLogs) { + Log.d(TAG, "Number of objects in POOL while after recycle(): " + mPool.size()); + } + } + } + + private synchronized ILogToken createTokenLocked() { + return new Snippet.LogToken(); + } +} diff --git a/snippet/src/main/java/com/microsoft/snippet/OneShot.java b/snippet/src/main/java/com/microsoft/snippet/OneShot.java new file mode 100644 index 0000000..331e112 --- /dev/null +++ b/snippet/src/main/java/com/microsoft/snippet/OneShot.java @@ -0,0 +1,54 @@ +/* + * Copyright © Microsoft Corporation. All rights reserved. + */ + +package com.microsoft.snippet; + +import android.util.Log; + +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; + +/** + * A thread safe implementation which let's the caller thread to set the value of the wrapping + * type once using {@link OneShot#set(Object)}, once the value is set, any other calls to set + * is not honoured. The value remains the same for the lifetime of the instance. + * Call {@link OneShot#get()} to access the wrapped instance. + * + * @param + * @author vishalratna + */ +public final class OneShot { + private static final String TAG = OneShot.class.getSimpleName(); + + private final AtomicReference mData; + private final AtomicInteger mCounter; + + public OneShot(T data) { + this.mData = new AtomicReference<>(data); + this.mCounter = new AtomicInteger(0); + } + + public void set(T data) { + if (mCounter.compareAndSet(0, 1)) { + T oldVal = mData.get(); + if (mData.compareAndSet(oldVal, data)) { + if (Snippet.mPrintDebugLogs) { + Log.d(TAG, "OneShot value set successfully."); + } + } else { + if (Snippet.mPrintDebugLogs) { + Log.e(TAG, "OneShot already set after the counter was set to 1. Cannot change the value again."); + } + } + } else { + if (Snippet.mPrintDebugLogs) { + Log.e(TAG, "OneShot already set once. Cannot change the value again."); + } + } + } + + public T get() { + return mData.get(); + } +} diff --git a/snippet/src/main/java/com/microsoft/snippet/Pair.java b/snippet/src/main/java/com/microsoft/snippet/Pair.java new file mode 100644 index 0000000..c874daf --- /dev/null +++ b/snippet/src/main/java/com/microsoft/snippet/Pair.java @@ -0,0 +1,28 @@ +/* + * Copyright © Microsoft Corporation. All rights reserved. + */ + +package com.microsoft.snippet; + +/** + * Represents a Pair of objects + * @param A + * @param B + */ +public final class Pair { + A a; + B b; + + public Pair(A first, B second) { + this.a = first; + this.b = second; + } + + public A getFirst() { + return a; + } + + public B getSecond() { + return b; + } +} \ No newline at end of file diff --git a/snippet/src/main/java/com/microsoft/snippet/ReleaseExecutionPath.java b/snippet/src/main/java/com/microsoft/snippet/ReleaseExecutionPath.java new file mode 100644 index 0000000..28614a1 --- /dev/null +++ b/snippet/src/main/java/com/microsoft/snippet/ReleaseExecutionPath.java @@ -0,0 +1,45 @@ +/* + * Copyright © Microsoft Corporation. All rights reserved. + */ + +package com.microsoft.snippet; + +import androidx.annotation.NonNull; + +import com.microsoft.snippet.token.ILogToken; + +/** + * This is the NOOP execution path that is used by Release build types. + */ +public class ReleaseExecutionPath implements ExecutionPath { + private static final ExecutionContext RESULT = new ExecutionContext(); + + @Override + @NonNull + public ExecutionContext capture(String message, Snippet.Closure closure) { + closure.invoke(); + return RESULT; + } + + @Override + @NonNull + public ExecutionContext capture(Snippet.Closure closure) { + closure.invoke(); + return RESULT; + } + + @Override + public ILogToken startCapture() { + return Snippet.NO_OP_TOKEN; + } + + @Override + public ILogToken startCapture(String tag) { + return Snippet.NO_OP_TOKEN; + } + + @Override + public ILogToken find(String tag) { + return Snippet.NO_OP_TOKEN; + } +} diff --git a/snippet/src/main/java/com/microsoft/snippet/Snippet.java b/snippet/src/main/java/com/microsoft/snippet/Snippet.java new file mode 100644 index 0000000..4fab8f5 --- /dev/null +++ b/snippet/src/main/java/com/microsoft/snippet/Snippet.java @@ -0,0 +1,740 @@ +/* + * Copyright © Microsoft Corporation. All rights reserved. + */ + +package com.microsoft.snippet; + +import android.os.Looper; +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.microsoft.snippet.token.AttenuatedLogToken; +import com.microsoft.snippet.token.ILogToken; +import com.microsoft.snippet.token.LogTokenState; + + +/** + * Snippet is a small library which can be used for measuring the time taken to execute a code + * snippet. This library intends to reduce the boiler place code needed to add the start and end timestamps, + * adding the execution context such as class,method and line number or any other relevant data and + * printing the logs. The work is tedious and it becomes a nightmare when we have to do it across the + * code base. The API provided by this library could be used to monitor PRs in the PR reviews where + * the reviewer could ask for numbers for a specific snippet and compare the before and after numbers. + *

There are 2 ways to measure time in Snippet

+ *
    + *
  1. Using capture APIs of Snippet
  2. + *
  3. Using log tokens and startCapture and endCapture APIs
  4. + *
+ *

+ * Example usage with Snippet.capture: + *

+ * {@code
+ *     Snippet.capture(() -> {
+ *             final ActionBar actionBar = getSupportActionBar();
+ *             if (actionBar != null && actionBar.isShowing()) {
+ *                 if (mToolbar != null) {
+ *                     ViewCompat.setElevation((View) mToolbar.getParent(), 0F);
+ *                     mToolbar.animate()
+ *                             .translationY(-mToolbar.getHeight())
+ *                             .setInterpolator(new AccelerateDecelerateInterpolator())
+ *                             .withEndAction(new Runnable() {
+ *                                 {@literal}Override
+ *                                 public void run() {
+ *                                     actionBar.hide();
+ *                                     if (mToolbarTranslateYListener != null) {
+ *                                         mToolbarTranslateYListener.onAnimationFinished();
+ *                                     }
+ *                                 }
+ *                             })
+ *                             .start();
+ *                 } else {
+ *                     actionBar.hide();
+ *                 }
+ *             }
+ *         });
+ * }
+ * 
+ * Here library is going to measure time used to execute the code represented by the underlying lambda. + *

+ * When you intent to measure the execution times for code which is not spread across multiple + * methods and classes, capture APIs can be used. There is one caveat though, while capturing, we pass + * a lambda for the closure representing the code snippet, this might be a problem if we want to capture + * some non final variables outside the scope of lambda(Java does not support that). For that use, {@link Final} to create a + * wrapper around your variable and use {@link Final#get()} and {@link Final#set(Object)}()} methods. + *

+ * Another approach is through {@link Snippet#startCapture()} & {@link LogToken#endCapture()} APIs. + * startCapture is going to return a token representing your execution it can be passed + * from one component to another. The moment you call endCapture() it will measure the + * time and print the logs. + *

+ * Example usage of Snippet.startCapture() and LogToken.endCapture(): + *

+ *     {@code
+ *      Snippet.LogToken token = Snippet.startCapture();        // start the measurement
+ *         getEditText().setOnTouchListener((v, event) -> {
+ *             mIsQueryTextFromVoiceInput = false;
+ *             if (!TextUtils.isEmpty(getEditText().getText())) {
+ *                 updateClearIconAndVoiceEntryVisibility(VISIBLE, GONE);
+ *             }
+ *
+ *             if (mOnEditTextTouchListener != null) {
+ *                 return mOnEditTextTouchListener.onTouch(v, event);
+ *             }
+ *
+ *             return false;
+ *         });
+ *
+ *         initVoiceInputEntry(context);
+ *         token.endCapture();  // end measurement and print logs.
+ *     }
+ * 
+ * We can use startCapture() method with a TAG also. This is particularly useful when + * the execution is spread across multiple classes. We can use, {@link Snippet#find(String)} to find + * the log token that was created with this tag. The logtoken received can be then used normally. + * {@link LogToken} objects are internally obtained through a pool so, Snippet tries it best to + * recycle the objects again and again. + *

+ * There is concept of filter, which can be used to filter the logcat output. You can set the filter + * using {@link Snippet#newFilter(String)}. Default filter is Snippet + * While this is global filter for all the logs, you can still choose to have a different filter for a + * particular LogToken using {@link LogToken#overrideFilter(String)} which will override the global filter. + *

+ * Snippet can print Class, Method and Line number information in the logs as a part of execution + * context. By default it prints class and method name. You can choose to have your combination of + * details through {@link Snippet#addFlag(int)} + * Valid options are: + *

    + * + *
  1. {@link Snippet#FLAG_METADATA_CLASS}
  2. + * + *
  3. {@link Snippet#FLAG_METADATA_METHOD}
  4. + * + *
  5. {@link Snippet#FLAG_METADATA_LINE}
  6. + * + *
  7. {@link Snippet#FLAG_NONE}
  8. + *
+ *

+ * While these information should be enough but if you need to add some more information to the logs + * like variable values, states etc then use {@link Snippet#capture(String, Closure)} and + * {@link LogToken#endCapture(String)} overloads for adding custom messages to the logcat + * output. + *

+ * Snippet also supports the concept of ThreadLocks. ThreadLocks is a feature where a log token + * can declare the thread creating the log token would only be able to call endCapture(). If other + * thread tries to call endCapture(). It will log an error. To enable thread locks one should + * call {@link LogToken#enableThreadLock()} and to check if the ThreadLock is enabled or not + * {@link LogToken#isThreadLockEnabled()} can be used. + *

+ * + *

+ * There are times while you are measuring a sequence of code and you would like to measure how + * much time some steps take within that sequence. As an example, while you are measuring some + * method that takes 300ms to execute, it could have multiple areas inside it that could be adding up + * to that number. So, to measure that {@link LogToken#addSplit()} and {@link LogToken#addSplit(String)} + * could be used. Each call to add split print the time takes since the last addSplit() was called. + * If addSplit() is called for the first time, then it would measure the time from startCapture(). + *

+ * + * @author vishalratna + */ +public final class Snippet { + private static final String TAG = LogToken.class.getSimpleName(); + static final AttenuatedLogToken NO_OP_TOKEN = new AttenuatedLogToken(); + + public static final int FLAG_METADATA_CLASS = 1 << 31; + public static final int FLAG_METADATA_METHOD = 1 << 30; + public static final int FLAG_METADATA_LINE = 1 << 29; + public static final int FLAG_METADATA_THREAD_INFO = 1 << 28; + public static final int FLAG_NONE = 0; + + private static String primaryFilter = Snippet.class.getSimpleName(); + private static final LogTokenPool OBJECT_POOL; + private static String packageNameFilter = "com.microsoft"; + private static StackAnalyser stackAnalyser = new StackAnalyser(packageNameFilter); + private static final TagHelper TAG_HELPER; + private static int mFlags = FLAG_METADATA_CLASS | FLAG_METADATA_METHOD; + private static final String SEPARATOR = "|::::|"; + private static final OneShot SHOULD_PRINT_DEBUG_LOGS = new OneShot<>(false); + private static final OneShot EXECUTION_PATH = new OneShot(new ReleaseExecutionPath()); // Release is the default execution path + static boolean mPrintDebugLogs = unBox(SHOULD_PRINT_DEBUG_LOGS.get()); // Do not set the value from anywhere other than turnOn/Off logs. Just meant for easy reference. + + static { + TAG_HELPER = new TagHelper(); + OBJECT_POOL = new LogTokenPool(); + } + + private Snippet() { + } + + /** + * Installs custom execution path. It is best to call this as early as possible. + * Prior to this call, core functionality will be routed to Release Execution path which is + * default. Can be set once. Attempts to set it multiple times will not be honoured. + * + * @param path execution path. + */ + public static void install(ExecutionPath path) { + EXECUTION_PATH.set(path); + } + + /** + * Captures a closure which needs to be measured. + * + * @param message Custom message if any. + * @param closure Lambda or implementation representing the closure. + */ + public static ExecutionContext capture(String message, Closure closure) { + return EXECUTION_PATH.get().capture(message, closure); + } + + /** + * Captures a closure which needs to be measured when no custom messages are required. + * + * @param closure Lambda or implementation representing the closure. + */ + public static ExecutionContext capture(Closure closure) { + return EXECUTION_PATH.get().capture(null, closure); + } + + /** + * Snippet identifies the execution context by analysing the stack frames and examining private + * members of {@link StackTraceElement} class. Out of dozens of stack frames containing JDK + * and android SDK frames, it has to identify the last frame which was related to the user application. + * For that uses the regex matching this filter to identify the user's application frame. + * After receiving the new regex it replaces the old stack analyser component with the new one. + * This impacts stack analyser of the log token also. Any log token created after this call will + * use the new regex, but token created before this call will have this one. + * + * @param regex REGEX identifying the application. + */ + public static void setPackageRegex(String regex) { + packageNameFilter = regex; + stackAnalyser = new StackAnalyser(packageNameFilter); + } + + private static ExecutionContext invokeMeasureAndAttachExecutionContext(String message, Closure closure) { + long delta = ToolBox.invokeAndMeasure(closure); + ExecutionContext executionContext = getExecutionContext(); + executionContext.setExecutionDuration(delta); + + // Build the log string using the snippet info we got. + StringBuilder logMessageBuilder = new StringBuilder(); + if (message != null && !message.isEmpty()) { + logMessageBuilder.append(message).append("::"); + } + + appendExecutionContextToLog(logMessageBuilder, executionContext); + + logMessageBuilder.append(SEPARATOR).append('(').append(delta).append(" ms)"); + Log.d(primaryFilter, logMessageBuilder.toString()); + return executionContext; + } + + /** + * Returns the execution context in te form of SnippetInfo class. That can be returned and + * used by external clients too. + * @return snippet info. + */ + private static ExecutionContext getExecutionContext() { + + Thread thread = Thread.currentThread(); + + ExecutionContext info = new ExecutionContext(); + info.setClassName(stackAnalyser.callingClass(thread, StackAnalyser.API_CAPTURE)); + info.setMethod(stackAnalyser.callingMethod(thread, StackAnalyser.API_CAPTURE)); + info.setLineNo(stackAnalyser.callingLine(thread, StackAnalyser.API_CAPTURE)); + info.setThreadName(thread.getName()); + + return info; + } + + private static void appendExecutionContextToLog(StringBuilder logMessageBuilder, ExecutionContext context) { + if (testFlag(FLAG_METADATA_CLASS)) { + if (Snippet.mPrintDebugLogs) { + Log.d(TAG, "FLAG_METADATA_CLASS set"); + } + String trimmedClass = trimPackageFromClass(context.getClassName()); + logMessageBuilder.append("[Class = ").append(trimmedClass).append(']').append(SEPARATOR); + } + if (testFlag(FLAG_METADATA_METHOD)) { + if (Snippet.mPrintDebugLogs) { + Log.d(TAG, "FLAG_METADATA_METHOD set"); + } + logMessageBuilder.append("[Method = ").append(context.getMethodName()).append(']').append(SEPARATOR); + } + if (testFlag(FLAG_METADATA_LINE)) { + if (Snippet.mPrintDebugLogs) { + Log.d(TAG, "FLAG_METADATA_LINE set"); + } + logMessageBuilder.append("').append(SEPARATOR); + } + if (testFlag(FLAG_METADATA_THREAD_INFO)) { + if (Snippet.mPrintDebugLogs) { + Log.d(TAG, "FLAG_METADATA_THREAD_INFO set"); + } + logMessageBuilder.append("[Thread name = ").append(Thread.currentThread().getName()).append(']').append(SEPARATOR); + } + + } + + private static String trimPackageFromClass(String qualifiedName) { + String[] tokens = qualifiedName.split("\\."); + if (Snippet.mPrintDebugLogs) { + StringBuilder temp = new StringBuilder(); + for (String a : tokens) { + temp.append('[').append(a).append("] "); + } + Log.d(TAG, "trimPackageFromClass() tokens: " + temp.toString()); + } + return tokens[tokens.length - 1]; + } + + + /** + * Set a new global filter and returns the old filter, just in case if you need to restore it. + * + * @param newFilterKey new filter for logcat + * @return old filter value. + */ + public static String newFilter(String newFilterKey) { + String oldTag = primaryFilter; + primaryFilter = newFilterKey; + return oldTag; + } + + /** + * Adds a flag which determines the execution context shown on the logs such as class name, method + * and line no getting called on the logcat output. Supports + *
    + * *
  1. {@link Snippet#FLAG_METADATA_CLASS}
  2. + * *
  3. {@link Snippet#FLAG_METADATA_METHOD}
  4. + * *
  5. {@link Snippet#FLAG_METADATA_LINE}
  6. + * *
  7. {@link Snippet#FLAG_NONE}
  8. + * *
+ * + * @param flag Integer representing the flags + * @return new flag value. + */ + public static int addFlag(int flag) { + //assureCorrectFlag(flag); // This will prevent us from supplying compound flags. + mFlags |= flag; + return mFlags; + } + + private static void assureCorrectFlag(int flag) { + if (flag != 1 << 31 && flag != 1 << 30 && flag != 1 << 29 && flag != 1 << 28) { + throw new IllegalArgumentException("Please set a valid flag"); + } + if (Snippet.mPrintDebugLogs) { + Log.d(TAG, "Flag validation completed."); + } + } + + /** + * Checks whether a particular flag is set in the Snippet or not. + * + * @param flag Flag to test + * @return true if set, false otherwise. + */ + public static boolean testFlag(int flag) { + assureCorrectFlag(flag); + return (mFlags & flag) == flag; + } + + /** + * Clears the flag which shows execution context. + * Calling this will not show any execution context in the logs. + */ + public static void clearFlags() { + mFlags = FLAG_NONE; + } + + /** + * Starts the measurement of code spread across multiple classes and methods. It returns a + * LogToken which can be passed across class and methods. To end the measurement call {@link LogToken#endCapture()} + * + * @return LogToken + */ + @NonNull + public static ILogToken startCapture() { + return EXECUTION_PATH.get().startCapture(); + } + + + /** + * Starts the measurement of code spread across multiple classes and methods. It returns a + * LogToken which can be passed as well as retrieved using a tag which is provided at the + * time of token creation. It enables a direct access for the log token anywhere in the code. + * If a token with the provided tag already exists, this call with return null which will make + * further calls to {@link LogToken#endCapture()} crash. + * If multiple threads simultaneously try to call this method with same tag, the first one to acquire the tag + * wins and the other gets Pair object with supplied log token and a successFlag as false . + * The token acquired in the initial step of this method is returned to the pool. + * + * @param tag tag + * @return LogToken which can we retrieved using the tag if request successful, null otherwise + */ + @Nullable + public static ILogToken startCapture(String tag) { + return EXECUTION_PATH.get().startCapture(tag); + } + + /** + * Finds a token with provided tag. The tag should match the tag provided with + * {@link Snippet#startCapture(String)} or else Attenuated token is returned. + * + * @param tag Tag + * @return LogToken if existing, attenuated token otherwise. + */ + public static ILogToken find(String tag) { + return EXECUTION_PATH.get().find(tag); + } + + public static void turnOnLogging() { + SHOULD_PRINT_DEBUG_LOGS.set(true); + mPrintDebugLogs = unBox(SHOULD_PRINT_DEBUG_LOGS.get()); + } + + public static void turnOffLogging() { + SHOULD_PRINT_DEBUG_LOGS.set(false); + mPrintDebugLogs = unBox(SHOULD_PRINT_DEBUG_LOGS.get()); + } + + private static boolean unBox(Boolean aBoolean) { + if (aBoolean == null) { + return false; + } + return aBoolean; + } + + /** + * Piece of code whose execution times need to be measured. + */ + public interface Closure { + /** + * Invokes the code which is passed inside the closure. + * Users are not supposed to call this method directly. + */ + void invoke(); + } + + /** + * When the execution spreads across multiple classes and methods, and lambda cannot be used + * to capture the code, then LogToken is created by calling {@link Snippet#startCapture()}, the + * measurement start from this point, now we can pass the token anywhere + * and, will end the measurement when {@link LogToken#endCapture()} is called. + * Default filter for a log token is the global filter of Snippet. + */ + public static class LogToken implements ILogToken { + private static final String TAG = LogToken.class.getSimpleName(); + private static final String SPLIT_MESSAGE = "********SPLIT[" + "%1s" + "]" + + SEPARATOR + "(" + "%2s" + " " + "ms" + + ")" + "********"; + + private final StackAnalyser mLocalAnalyser = new StackAnalyser(packageNameFilter); + private long mStartTime; + private long mEndTime; + private String mFilter; + private long mThreadId = -1L; + private boolean mThreadLockEnabled = false; + private LogTokenState mState; + private long mLastSplitTimeCaptured = 0L; + private int mSplitCount = 0; + + // To be called only through LogTokenPool. Should not be created through any other ways. + protected LogToken() { + if (mPrintDebugLogs) { + if (Thread.currentThread() == Looper.getMainLooper().getThread()) { + Log.d(TAG, "Inside LogToken's . startCapture() called on the main thread. LogToken[" + this.toString() + "]"); + } else { + Log.d(TAG, "Inside LogToken's . startCapture() called off the main thread. LogToken[" + this.toString() + "]"); + } + Log.d(TAG, " will be called once per LogToken object, after the usage is over, it will be returned to the pool and will get recycled using obtain()"); + } + this.mStartTime = ToolBox.currentTime(); + this.mFilter = Snippet.primaryFilter; // Uses the primary filter by default. + } + + /** + * Can be used to override the Global filter provided by Snippet. + * Once this LogToken's endCapture() is called, the filter is reset to + * the global filter again. + * + * @param newFilter new filter + * @return LogToken instance on which start()/end() can be called. + */ + @Override + public LogToken overrideFilter(String newFilter) { + this.mFilter = newFilter; + return this; + } + + /** + * Returns current filter + * + * @return existing filter + */ + @Override + public String filter() { + return this.mFilter; + } + + @Override + public long creatorThreadId() { + return this.mThreadId; + } + + @Override + public boolean isThreadLockEnabled() { + return mThreadLockEnabled; + } + + @Override + public ILogToken enableThreadLock() { + mThreadLockEnabled = true; + return this; + } + + public void setCreatorThreadId(long id) { + this.mThreadId = id; + } + + @Override + public void reset() { + this.mStartTime = 0; + this.mEndTime = 0; + this.mFilter = Snippet.primaryFilter; + this.mThreadId = -1L; + this.mThreadLockEnabled = false; + this.mSplitCount = 0; + } + + /** + * Creates a split within the span of LogToken's startCapture() and endCapture() methods. + * Calling addSplit() will measure the time taken since the last time addSplit() was called. + * If addSplit() is called for the first time then it would measure the time between + * {@link Snippet#startCapture()} and {@link LogToken#endCapture()}. Calling addSplit() after + * endCapture() is called would lead to IllegalStateException. + */ + @Override + public void addSplit() { + // This is not fully true but a quick hack, will have to think about multiple states possible. + if (mState != LogTokenState.ACTIVE) { + throw new IllegalStateException("addSplit() called after endCapture() is executed! Development error!!!!!!"); + } + long splitInMillis = getSplitInMillis(); + mSplitCount++; + Log.d(mFilter, String.format(SPLIT_MESSAGE, mSplitCount, splitInMillis)); + } + + /** + * Does the same thing as {@link LogToken#addSplit()}. It just additionally adds a message string + * with the Split. + * + * @param message Custom message to print on the log cat. + */ + @Override + public void addSplit(String message) { + if (mState != LogTokenState.ACTIVE) { + throw new IllegalStateException("addSplit() called after endCapture() is executed! Development error!!!!!!"); + } + long splitInMillis = getSplitInMillis(); + mSplitCount++; + Log.d(mFilter, String.format(SPLIT_MESSAGE, "[" + mSplitCount + "]" + message, splitInMillis)); + } + + private long getSplitInMillis() { + long delta; + if (mLastSplitTimeCaptured == 0L) { // Split called for the first time + // We use the token start time as the reference. + mLastSplitTimeCaptured = ToolBox.currentTime(); + delta = mLastSplitTimeCaptured - getStart(); + } else { + // Here we use the last split time captured. + long temp = ToolBox.currentTime(); + delta = temp - mLastSplitTimeCaptured; + mLastSplitTimeCaptured = temp; + } + return delta; + } + + @Override + public void setState(LogTokenState state) { + mState = state; + } + + @Override + public LogTokenState getState() { + return mState; + } + + /** + * Ends the capture which was started through {@link Snippet#startCapture()}. + */ + @Override + public ExecutionContext endCapture() { + ExecutionContext executionContext; + if (ToolBox.willThreadLockGuardThisCapture(Thread.currentThread(), this)) { + Log.e(TAG, "ThreadLocks enabled! Not able to end the capture as the token " + + "creating thread is not same as the thread calling endCapture()."); + + throw new UnsupportedOperationException("endCapture() should always be called on " + + "the same thread that created log token with Snippet.startCapture() while " + + "thread locks are enabled"); + } else { + mState = LogTokenState.END_CAPTURE_EXECUTED; + executionContext = doEndSlice(null); + ILogToken token = TAG_HELPER.unTag(this); + if (token == null) { + Log.e(TAG, "Not able to unTag as the tag for the request was not available."); + } + OBJECT_POOL.recycle(this); + return executionContext; + } + } + + /** + * Ends the capture which was started through {@link Snippet#startCapture()}. + * + * @param message Custom message if required + */ + @Override + public ExecutionContext endCapture(String message) { + ExecutionContext info; + if (ToolBox.willThreadLockGuardThisCapture(Thread.currentThread(), this)) { + Log.e(TAG, mFilter + " ThreadLocks enabled! Not able to end the capture as the" + + " token creating thread is not same as the thread calling endCapture(message)."); + + throw new UnsupportedOperationException("endCapture() should always be called on " + + "the same thread that created log token with Snippet.startCapture() while " + + "thread locks are enabled"); + } else { + mState = LogTokenState.END_CAPTURE_EXECUTED; + info = doEndSlice(message); + ILogToken token = TAG_HELPER.unTag(this); + if (token == null) { + Log.d(TAG, "Not able to unTag as the tag for the request was not available."); + } + OBJECT_POOL.recycle(this); + return info; + } + } + + private ExecutionContext doEndSlice(String message) { + synchronized (this) { + mEndTime = ToolBox.currentTime(); + long delta = mEndTime - mStartTime; + + StringBuilder logMessageBuilder = new StringBuilder(); + if (message != null && !message.isEmpty()) { + logMessageBuilder.append(message).append(SEPARATOR); + } + Thread thread = Thread.currentThread(); + if (mPrintDebugLogs) { + if (thread == Looper.getMainLooper().getThread()) { + Log.d(TAG, "endCapture() called on the main thread. LogToken[" + this.toString() + "]"); + } else { + Log.d(TAG, "endCapture() called off the main thread"); + } + } + ExecutionContext executionContext = new ExecutionContext(); + executionContext.setClassName(mLocalAnalyser.callingClass(thread, StackAnalyser.API_LOG_TOKEN)); + executionContext.setMethod(mLocalAnalyser.callingMethod(thread, StackAnalyser.API_LOG_TOKEN)); + executionContext.setLineNo(mLocalAnalyser.callingLine(thread, StackAnalyser.API_LOG_TOKEN)); + executionContext.setThreadName(thread.getName()); + executionContext.setExecutionDuration(delta); + + appendExecutionContextToLog(logMessageBuilder, executionContext); + logMessageBuilder.append(SEPARATOR).append('(').append(delta).append(" ms)"); + Log.d(mFilter, logMessageBuilder.toString()); + return executionContext; + } + } + + @Override + public final long getStart() { + return mStartTime; + } + + @Override + public final long getEnd() { + return mEndTime; + } + + @Override + public final void setStart(long start) { + this.mStartTime = start; + } + + @Override + public final void setEnd(long start) { + this.mStartTime = start; + } + } + + + /** + * Execution path that is used by Snippet to route the code to the core library functionality. + */ + public static class MeasuredExecutionPath implements ExecutionPath { + + @Override + @NonNull + public ExecutionContext capture(String message, Closure closure) { + return invokeMeasureAndAttachExecutionContext(message, closure); + } + + @Override + @NonNull + public ExecutionContext capture(Closure closure) { + return invokeMeasureAndAttachExecutionContext(null, closure); + } + + @Override + public ILogToken startCapture() { + long startTime = ToolBox.currentTime(); + ILogToken token = OBJECT_POOL.obtain(); + token.setStart(startTime); + token.setCreatorThreadId(Thread.currentThread().getId()); + return token; + } + + @Override + public ILogToken startCapture(String tag) { + long startTime = ToolBox.currentTime(); + ILogToken token = OBJECT_POOL.obtain(); + token.setStart(startTime); + token.setCreatorThreadId(Thread.currentThread().getId()); + Pair tagResult = TAG_HELPER.tag(tag, token); + if (!tagResult.getSecond()) { + Log.e(TAG, "Tag: [" + tag + "] already exists in the record, cannot assign log token, so we are providing a NO_OP_TOKEN."); + OBJECT_POOL.recycle(token); + return NO_OP_TOKEN; + } + if (mPrintDebugLogs) { + Log.e(TAG, "Tag: [" + tag + "] created for the LogToken."); + } + return token; + } + + /** + * Used to find Log Token that was created using {@link Snippet#startCapture(String)} + * If we try to find a tag that does not exist it would return {@link Snippet#NO_OP_TOKEN} + * that is a NO-OP implementation of {@link ILogToken} interface. + * + * @param tag Custom tag. This tag will be used to search the log token across the app. + * @return LogToken if existing with the tag, No op token otherwise. + */ + @Override + public ILogToken find(String tag) { + ILogToken token = TAG_HELPER.search(tag); + if (token == null) { + return NO_OP_TOKEN; + } else { + return token; + } + } + } +} diff --git a/snippet/src/main/java/com/microsoft/snippet/SnippetConstants.java b/snippet/src/main/java/com/microsoft/snippet/SnippetConstants.java new file mode 100644 index 0000000..3542fd7 --- /dev/null +++ b/snippet/src/main/java/com/microsoft/snippet/SnippetConstants.java @@ -0,0 +1,32 @@ +/* + * Copyright © Microsoft Corporation. All rights reserved. + */ + +package com.microsoft.snippet; + +/** + * Contains the list of areas that are tracked by Snippet. These will help getting the before/after + * numbers while doing the PR review. + */ +public final class SnippetConstants { + + private SnippetConstants() { + // private + } + + // TTE stands for Time to Execute + public static final String SUPER_ON_MAM_CREATE = "TTE super.onMamCreate()"; + public static final String INIT_SKYPE_DB_HELPER = "TTE SkypeTeamsDBHelper"; + public static final String INIT_BACKGROUND_OBJ = "TTE Initialize background objects"; + public static final String INIT_NOTIFICATION_CHANNEL_HELPER = "TTE Initialize notificationChannelHelper"; + public static final String INIT_NOTIFICATION_MGR = "TTE Initialize notification manager"; + public static final String SUPER_MA_ON_CREATE = "TTE super.onCreate() MA"; + public static final String INIT_SEARCH_BAR_VIEW = "TTE SearchBarView MA"; + public static final String SETUP_CONTENT_VIEW = "TTE SetupContentView"; + public static final String INJECT_DEPENDENCIES = "TTE injectIfNecessary"; + public static final String SETUP_TOOLBAR = "TTE setupToolBar"; + public static final String MA_LOAD_INACTIVE_TABS_UI_THREAD_WORK = "TTE loadInactiveTabs() UI thread Work"; + public static final String MA_LOAD_SELECTED_FRAGMENT = "TTE load selected fragment"; + public static final String DEPENDENCY_INJECTION = "TTE Dependency injection"; + public static final String BOTTOM_BAR_APP_CLICK_TELEMETRY = "Bottom Bar App Click Telemetry"; +} diff --git a/snippet/src/main/java/com/microsoft/snippet/StackAnalyser.java b/snippet/src/main/java/com/microsoft/snippet/StackAnalyser.java new file mode 100644 index 0000000..6d5862a --- /dev/null +++ b/snippet/src/main/java/com/microsoft/snippet/StackAnalyser.java @@ -0,0 +1,76 @@ +/* + * Copyright © Microsoft Corporation. All rights reserved. + */ + +package com.microsoft.snippet; + +/** + * Internal helper class used to extract the execution context of the code which was guarded + * by Snippet APIs. + * NOT FOR EXTERNAL USE + */ +final class StackAnalyser { + static final int API_CAPTURE = 0; + static final int API_LOG_TOKEN = 1; + + private final String mPackage; + + StackAnalyser(String packageName) { + this.mPackage = packageName; + } + + StackAnalyser() { + this("com.microsoft"); + } + + String callingMethod(Thread mCallingThread, int apiType) { + if (apiType == API_CAPTURE) { + return getCallingFrameForCapture(mCallingThread, apiType).getMethodName(); + } else { + return getDoEndSliceCallerFrame(mCallingThread, apiType).getMethodName(); + } + } + + String callingClass(Thread mCallingThread, int apiType) { + if (apiType == API_CAPTURE) { + return getCallingFrameForCapture(mCallingThread, apiType).getClassName(); + } else { + return getDoEndSliceCallerFrame(mCallingThread, apiType).getClassName(); + } + } + + int callingLine(Thread mCallingThread, int apiType) { + if (apiType == API_CAPTURE) { + return getCallingFrameForCapture(mCallingThread, apiType).getLineNumber(); + } else { + return getDoEndSliceCallerFrame(mCallingThread, apiType).getLineNumber(); + } + } + + private StackTraceElement searchForCallingFrameOfMethodCaller(Thread callingThread, String methodName, int apiType) { + StackTraceElement[] frames = callingThread.getStackTrace(); + int index = -1; + for (int i = 0; i < frames.length; i++) { + if (frames[i].getMethodName().equals(methodName) + && frames[i].getClassName().startsWith(mPackage) && apiType == API_CAPTURE + && frames[i].getClassName().equals("com.microsoft.snippet.Snippet") + || + frames[i].getMethodName().equals(methodName) + && frames[i].getClassName().startsWith(mPackage) && apiType == API_LOG_TOKEN + && frames[i].getClassName().equals("com.microsoft.snippet.Snippet$LogToken")) { + index = i; + break; + } + } + // Now we know the index at which we found the method in the stackframe, the next stack frame belongs to the code that called that method. + return callingThread.getStackTrace()[index + 1]; + } + + private StackTraceElement getDoEndSliceCallerFrame(Thread thread, int apiType) { + return searchForCallingFrameOfMethodCaller(thread, "endCapture", apiType); + } + + private StackTraceElement getCallingFrameForCapture(Thread thread, int apiType) { + return searchForCallingFrameOfMethodCaller(thread, "capture", apiType); + } +} diff --git a/snippet/src/main/java/com/microsoft/snippet/TagHelper.java b/snippet/src/main/java/com/microsoft/snippet/TagHelper.java new file mode 100644 index 0000000..84a47ee --- /dev/null +++ b/snippet/src/main/java/com/microsoft/snippet/TagHelper.java @@ -0,0 +1,93 @@ +/* + * Copyright © Microsoft Corporation. All rights reserved. + */ + +package com.microsoft.snippet; + +import android.util.Log; + +import androidx.annotation.NonNull; + +import com.microsoft.snippet.token.ILogToken; + +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +/** + * Internal helper class that takes care of tagging the LogTokens. + * Not intended for external use. + * + * @author vishalratna + */ +class TagHelper implements ILogTokenSearcher { + private static final String LOG_TAG = TagHelper.class.getSimpleName(); + + private final Map mRegistry; + + TagHelper() { + this.mRegistry = new HashMap<>(); + } + + @NonNull + Pair tag(String tag, ILogToken token) { + synchronized (mRegistry) { + // Synchronizing to make sure, while we are checking the existence, a new thread does + // not add a key which returns true for contains() call. + if (mRegistry.containsKey(tag)) { + if (Snippet.mPrintDebugLogs) { + Log.e(LOG_TAG, "Tag already existing, we will not provide tagged token. Returning the token without tagging."); + } + return new Pair<>(token, false); + } + + // Now we know that entry is not existing, make an entry + if (Snippet.mPrintDebugLogs) { + Log.d(LOG_TAG, "Tagging the LogToken and returning it back"); + } + mRegistry.put(tag, token); + } + + return new Pair<>(token, true); + } + + ILogToken unTag(ILogToken token) { + synchronized (mRegistry) { + String requestedTag = null; + Set> entrySet = mRegistry.entrySet(); + for (Map.Entry eachEntry : entrySet) { + if (eachEntry.getValue() == token) { + requestedTag = eachEntry.getKey(); + break; + } + } + // If we did not get anything after searching the registry. + if (requestedTag == null) { + if (Snippet.mPrintDebugLogs) { + Log.e(LOG_TAG, "There is no such tagged LogToken existing. Bad request! Returning"); + } + return null; + } + + if (Snippet.mPrintDebugLogs) { + Log.e(LOG_TAG, "We found a existing tag for the LogToken " + token.toString() + ", removing the tag"); + } + return mRegistry.remove(requestedTag); + } + } + + @Override + public ILogToken search(String tag) { + synchronized (mRegistry) { + if (mRegistry.containsKey(tag)) { + return mRegistry.get(tag); + } else { + if (Snippet.mPrintDebugLogs) { + Log.d(LOG_TAG, "There is no log token with tag: " + tag); + } + return null; + } + } + } +} + diff --git a/snippet/src/main/java/com/microsoft/snippet/ToolBox.java b/snippet/src/main/java/com/microsoft/snippet/ToolBox.java new file mode 100644 index 0000000..660cdbc --- /dev/null +++ b/snippet/src/main/java/com/microsoft/snippet/ToolBox.java @@ -0,0 +1,59 @@ +/* + * Copyright © Microsoft Corporation. All rights reserved. + */ + +package com.microsoft.snippet; + +import android.os.SystemClock; + +import com.microsoft.snippet.token.ILogToken; + +/** + * Utility class the houses some helper functions used across the library. + */ +final class ToolBox { + + private ToolBox() { + + } + + static long invokeAndMeasure(Snippet.Closure closure) { + long start = SystemClock.uptimeMillis(); + closure.invoke(); + long end = SystemClock.uptimeMillis(); + + return end - start; + } + + static boolean willThreadLockGuardThisCapture(Thread currentThread, ILogToken token) { + if (token.isThreadLockEnabled()) { + return currentThread.getId() != token.creatorThreadId(); + } else { + return false; + } + } + + static String combineThreadIdWithUserTag(String tag) { + return tag + ":" + Thread.currentThread().getId(); + } + + static long getThreadIdFromSnippetTag(String snippetTag) { + String[] items = snippetTag.split(":"); + if (items.length != 2) { + throw new IllegalStateException("Snippet tag should have 2 items, user tag and thread ID"); + } + return Long.parseLong(items[1]); + } + + static String getUserTagFromSnippetTag(String snippetTag) { + String[] items = snippetTag.split(":"); + if (items.length != 2) { + throw new IllegalStateException("Snippet tag should have 2 items, user tag and thread ID"); + } + return items[0]; + } + + static long currentTime() { + return SystemClock.uptimeMillis(); + } +} diff --git a/snippet/src/main/java/com/microsoft/snippet/Triple.java b/snippet/src/main/java/com/microsoft/snippet/Triple.java new file mode 100644 index 0000000..5f6ac1c --- /dev/null +++ b/snippet/src/main/java/com/microsoft/snippet/Triple.java @@ -0,0 +1,36 @@ +/* + * Copyright © Microsoft Corporation. All rights reserved. + */ + +package com.microsoft.snippet; + +/** + * Represents a Triple of objects + * + * @param
A + * @param B + * @param C + */ +public final class Triple { + A a; + B b; + C c; + + public Triple(A first, B second, C third) { + this.a = first; + this.b = second; + this.c = third; + } + + public A getFirst() { + return a; + } + + public B getSecond() { + return b; + } + + public C getThird() { + return c; + } +} \ No newline at end of file diff --git a/snippet/src/main/java/com/microsoft/snippet/token/AttenuatedLogToken.java b/snippet/src/main/java/com/microsoft/snippet/token/AttenuatedLogToken.java new file mode 100644 index 0000000..ba0a69e --- /dev/null +++ b/snippet/src/main/java/com/microsoft/snippet/token/AttenuatedLogToken.java @@ -0,0 +1,105 @@ +/* + * Copyright © Microsoft Corporation. All rights reserved. + */ + +package com.microsoft.snippet.token; + +import com.microsoft.snippet.ExecutionContext; + +/** + * Attenuated token represents a LogToken with suppressed functionality. + * This is helpful in + * 1. Creating NO-OP implementations for our public facing APIs. + * 2. While using {@link com.microsoft.snippet.Snippet#find(String)} it might be possible that we + * ask for a tag that does not exist it would lead to a null result and would require the caller + * to use a null check. Using this there helps in returning a no op token and user can avoid null check + * 3. In some flows you won't always find logtokens for example, + * while creating a token with tag in onCreate() of Application class and trying to fetch it in + * some activity. As application onCreate() will not be called always, activity might need a + * null check after find call, that is avoided by the use of this class. + */ +public final class AttenuatedLogToken implements ILogToken { + private static final ExecutionContext NONE_INFO = new ExecutionContext(); + + public AttenuatedLogToken() { + } + + @Override + public ExecutionContext endCapture() { + return NONE_INFO; + } + + @Override + public long getStart() { + return -1; + } + + @Override + public long getEnd() { + return -1; + } + + @Override + public void setStart(long start) { + } + + @Override + public void setEnd(long start) { + } + + @Override + public ILogToken overrideFilter(String newFilter) { + return this; + } + + @Override + public String filter() { + return ""; + } + + @Override + public ExecutionContext endCapture(String message) { + return NONE_INFO; + } + + @Override + public long creatorThreadId() { + return -1L; + } + + @Override + public boolean isThreadLockEnabled() { + return false; + } + + @Override + public ILogToken enableThreadLock() { + return this; + } + + @Override + public void setCreatorThreadId(long threadId) { + } + + @Override + public void reset() { + } + + @Override + public void addSplit() { + } + + @Override + public void addSplit(String message) { + } + + @Override + public void setState(LogTokenState state) { + } + + @Override + public LogTokenState getState() { + return LogTokenState.ATTENUATED; + } + +} diff --git a/snippet/src/main/java/com/microsoft/snippet/token/ILogToken.java b/snippet/src/main/java/com/microsoft/snippet/token/ILogToken.java new file mode 100644 index 0000000..ba7219b --- /dev/null +++ b/snippet/src/main/java/com/microsoft/snippet/token/ILogToken.java @@ -0,0 +1,74 @@ +/* + * Copyright © Microsoft Corporation. All rights reserved. + */ + +package com.microsoft.snippet.token; + +import com.microsoft.snippet.Snippet; +import com.microsoft.snippet.ExecutionContext; + +/** + * Representation of log token. All types of log token should implement this interface + */ +public interface ILogToken { + /** + * Set the filter for this log token, this is override the global filter. + * + * @param newFilter new filter + * @return IToken instance on which start()/end() can be called. + */ + ILogToken overrideFilter(String newFilter); + + /** + * Returns existing filter + * + * @return existing filter + */ + String filter(); + + /** + * Ends the capture which was started through {@link Snippet#startCapture()}. + * + * @param message Custom message if required + */ + ExecutionContext endCapture(String message); + + /** + * Returns the id of the thread that asked for this log token. + * + * @return Thread ID. + */ + long creatorThreadId(); + + /** + * + */ + boolean isThreadLockEnabled(); + + ILogToken enableThreadLock(); + + void setCreatorThreadId(long threadId); + + void reset(); + + void addSplit(); + + void addSplit(String message); + + void setState(LogTokenState state); + + LogTokenState getState(); + + /** + * Ends the capture which was started through {@link Snippet#startCapture()}. + */ + ExecutionContext endCapture(); + + long getStart(); + + long getEnd(); + + void setStart(long start); + + void setEnd(long start); +} diff --git a/snippet/src/main/java/com/microsoft/snippet/token/LogTokenState.java b/snippet/src/main/java/com/microsoft/snippet/token/LogTokenState.java new file mode 100644 index 0000000..d7949e4 --- /dev/null +++ b/snippet/src/main/java/com/microsoft/snippet/token/LogTokenState.java @@ -0,0 +1,26 @@ +/* + * Copyright © Microsoft Corporation. All rights reserved. + */ + +package com.microsoft.snippet.token; + +public enum LogTokenState { + + ACTIVE(1), + + END_CAPTURE_EXECUTED(2), + + IN_POOL(3), + + ATTENUATED(4); + + private final int mId; + + LogTokenState(int id) { + this.mId = id; + } + + public int getID() { + return mId; + } +} diff --git a/snippet/src/main/res/drawable-v24/ic_launcher_foreground.xml b/snippet/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 0000000..2b068d1 --- /dev/null +++ b/snippet/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/snippet/src/main/res/drawable/ic_launcher_background.xml b/snippet/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..07d5da9 --- /dev/null +++ b/snippet/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/snippet/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/snippet/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..eca70cf --- /dev/null +++ b/snippet/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/snippet/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/snippet/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..eca70cf --- /dev/null +++ b/snippet/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/snippet/src/main/res/mipmap-hdpi/ic_launcher.png b/snippet/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..a571e60 Binary files /dev/null and b/snippet/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/snippet/src/main/res/mipmap-hdpi/ic_launcher_round.png b/snippet/src/main/res/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 0000000..61da551 Binary files /dev/null and b/snippet/src/main/res/mipmap-hdpi/ic_launcher_round.png differ diff --git a/snippet/src/main/res/mipmap-mdpi/ic_launcher.png b/snippet/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..c41dd28 Binary files /dev/null and b/snippet/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/snippet/src/main/res/mipmap-mdpi/ic_launcher_round.png b/snippet/src/main/res/mipmap-mdpi/ic_launcher_round.png new file mode 100644 index 0000000..db5080a Binary files /dev/null and b/snippet/src/main/res/mipmap-mdpi/ic_launcher_round.png differ diff --git a/snippet/src/main/res/mipmap-xhdpi/ic_launcher.png b/snippet/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..6dba46d Binary files /dev/null and b/snippet/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/snippet/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/snippet/src/main/res/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 0000000..da31a87 Binary files /dev/null and b/snippet/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/snippet/src/main/res/mipmap-xxhdpi/ic_launcher.png b/snippet/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..15ac681 Binary files /dev/null and b/snippet/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/snippet/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/snippet/src/main/res/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 0000000..b216f2d Binary files /dev/null and b/snippet/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/snippet/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/snippet/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..f25a419 Binary files /dev/null and b/snippet/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/snippet/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/snippet/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 0000000..e96783c Binary files /dev/null and b/snippet/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/snippet/src/main/res/values-night/themes.xml b/snippet/src/main/res/values-night/themes.xml new file mode 100644 index 0000000..9bb62c2 --- /dev/null +++ b/snippet/src/main/res/values-night/themes.xml @@ -0,0 +1,16 @@ + + + + \ No newline at end of file diff --git a/snippet/src/main/res/values/colors.xml b/snippet/src/main/res/values/colors.xml new file mode 100644 index 0000000..f8c6127 --- /dev/null +++ b/snippet/src/main/res/values/colors.xml @@ -0,0 +1,10 @@ + + + #FFBB86FC + #FF6200EE + #FF3700B3 + #FF03DAC5 + #FF018786 + #FF000000 + #FFFFFFFF + \ No newline at end of file diff --git a/snippet/src/main/res/values/strings.xml b/snippet/src/main/res/values/strings.xml new file mode 100644 index 0000000..96329db --- /dev/null +++ b/snippet/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + Snippet + \ No newline at end of file diff --git a/snippet/src/main/res/values/themes.xml b/snippet/src/main/res/values/themes.xml new file mode 100644 index 0000000..1a35b2b --- /dev/null +++ b/snippet/src/main/res/values/themes.xml @@ -0,0 +1,16 @@ + + + + \ No newline at end of file diff --git a/snippet/src/test/java/com/microsoft/snippet/ExampleUnitTest.java b/snippet/src/test/java/com/microsoft/snippet/ExampleUnitTest.java new file mode 100644 index 0000000..9fcd989 --- /dev/null +++ b/snippet/src/test/java/com/microsoft/snippet/ExampleUnitTest.java @@ -0,0 +1,17 @@ +package com.microsoft.snippet; + +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() { + assertEquals(4, 2 + 2); + } +} \ No newline at end of file