diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..aa724b7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +*.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 +.cxx +local.properties 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..7c8ed44 --- /dev/null +++ b/.idea/.name @@ -0,0 +1 @@ +Frankenstein-app \ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 0000000..b589d56 --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/deploymentTargetDropDown.xml b/.idea/deploymentTargetDropDown.xml new file mode 100644 index 0000000..0c0c338 --- /dev/null +++ b/.idea/deploymentTargetDropDown.xml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml new file mode 100644 index 0000000..e44c590 --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,19 @@ + + + + + + \ No newline at end of file diff --git a/.idea/migrations.xml b/.idea/migrations.xml new file mode 100644 index 0000000..f8051a6 --- /dev/null +++ b/.idea/migrations.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..0ad17cb --- /dev/null +++ b/.idea/misc.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..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..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..259fd11 --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,40 @@ +plugins { + id 'com.android.application' +} + +android { + namespace 'com.phonereplay.frankenstein_app' + compileSdk 34 + + defaultConfig { + applicationId "com.phonereplay.frankenstein_app" + minSdk 28 + targetSdk 34 + 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.6.1' + implementation 'com.google.android.material:material:1.11.0' + implementation 'androidx.constraintlayout:constraintlayout:2.1.4' + implementation project(':tasklogger') + testImplementation 'junit:junit:4.13.2' + androidTestImplementation 'androidx.test.ext:junit:1.1.5' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' +} \ 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/phonereplay/frankenstein_app/ExampleInstrumentedTest.java b/app/src/androidTest/java/com/phonereplay/frankenstein_app/ExampleInstrumentedTest.java new file mode 100644 index 0000000..a63dcaf --- /dev/null +++ b/app/src/androidTest/java/com/phonereplay/frankenstein_app/ExampleInstrumentedTest.java @@ -0,0 +1,26 @@ +package com.phonereplay.frankenstein_app; + +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.phonereplay.frankenstein_app", 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..0dccd3f --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/phonereplay/frankenstein_app/MainActivity.java b/app/src/main/java/com/phonereplay/frankenstein_app/MainActivity.java new file mode 100644 index 0000000..aba546b --- /dev/null +++ b/app/src/main/java/com/phonereplay/frankenstein_app/MainActivity.java @@ -0,0 +1,14 @@ +package com.phonereplay.frankenstein_app; + +import androidx.appcompat.app.AppCompatActivity; + +import android.os.Bundle; + +public class MainActivity extends AppCompatActivity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + } +} \ 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/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..2b068d1 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file 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..0b15a20 --- /dev/null +++ b/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi/ic_launcher.xml b/app/src/main/res/mipmap-anydpi/ic_launcher.xml new file mode 100644 index 0000000..6f3b755 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi/ic_launcher.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml new file mode 100644 index 0000000..6f3b755 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 0000000..c209e78 Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp new file mode 100644 index 0000000..b2dfe3d Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp new file mode 100644 index 0000000..4f0f1d6 Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp new file mode 100644 index 0000000..62b611d Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp new file mode 100644 index 0000000..948a307 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..1b9a695 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp new file mode 100644 index 0000000..28d4b77 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..9287f50 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp new file mode 100644 index 0000000..aa7d642 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..9126ae3 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp 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..8147eca --- /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..c7f340a --- /dev/null +++ b/app/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + Frankenstein-app + \ 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..fe70604 --- /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/main/res/xml/backup_rules.xml b/app/src/main/res/xml/backup_rules.xml new file mode 100644 index 0000000..fa0f996 --- /dev/null +++ b/app/src/main/res/xml/backup_rules.xml @@ -0,0 +1,13 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/data_extraction_rules.xml b/app/src/main/res/xml/data_extraction_rules.xml new file mode 100644 index 0000000..9ee9997 --- /dev/null +++ b/app/src/main/res/xml/data_extraction_rules.xml @@ -0,0 +1,19 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/test/java/com/phonereplay/frankenstein_app/ExampleUnitTest.java b/app/src/test/java/com/phonereplay/frankenstein_app/ExampleUnitTest.java new file mode 100644 index 0000000..f8297b0 --- /dev/null +++ b/app/src/test/java/com/phonereplay/frankenstein_app/ExampleUnitTest.java @@ -0,0 +1,17 @@ +package com.phonereplay.frankenstein_app; + +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..7e5e64c --- /dev/null +++ b/build.gradle @@ -0,0 +1,5 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. +plugins { +id 'com.android.application' version '8.2.2' apply false + id 'com.android.library' version '8.2.2' apply false +} \ No newline at end of file diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..3e927b1 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,21 @@ +# 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 +# Enables namespacing of each library's R class so that its R class includes only the +# resources declared in the library itself and none from the library's dependencies, +# thereby reducing the size of the R class for that library +android.nonTransitiveRClass=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..e708b1c 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..d54b5d1 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Sat Mar 30 01:08:00 BRT 2024 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..4f906e0 --- /dev/null +++ b/gradlew @@ -0,0 +1,185 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +## +## 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='"-Xmx64m" "-Xms64m"' + +# 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 or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; 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=`expr $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" + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..107acd3 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@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 Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@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="-Xmx64m" "-Xms64m" + +@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 execute + +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 execute + +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 + +: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 %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..ef41a78 --- /dev/null +++ b/settings.gradle @@ -0,0 +1,18 @@ +pluginManagement { + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + google() + mavenCentral() + } +} + +rootProject.name = "Frankenstein-app" +include ':app' +include ':tasklogger' diff --git a/tasklogger/.gitignore b/tasklogger/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/tasklogger/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/tasklogger/build.gradle b/tasklogger/build.gradle new file mode 100644 index 0000000..e0e0545 --- /dev/null +++ b/tasklogger/build.gradle @@ -0,0 +1,46 @@ +plugins { + id 'com.android.library' +} + +android { + namespace 'com.phonereplay.tasklogger' + compileSdk 34 + + defaultConfig { + minSdk 28 + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles "consumer-rules.pro" + } + + 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.6.1' + implementation 'com.google.android.material:material:1.11.0' + testImplementation 'junit:junit:4.13.2' + androidTestImplementation 'androidx.test.ext:junit:1.1.5' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' + + implementation 'com.google.code.gson:gson:2.10.1' + + // retrofit + implementation 'com.squareup.retrofit2:retrofit:2.9.0' + implementation 'com.squareup.retrofit2:converter-gson:2.9.0' + + implementation 'io.grpc:grpc-okhttp:1.60.1' + implementation 'io.grpc:grpc-protobuf-lite:1.60.1' + implementation 'io.grpc:grpc-stub:1.60.1' + implementation 'org.apache.tomcat:annotations-api:6.0.53' +} \ No newline at end of file diff --git a/tasklogger/consumer-rules.pro b/tasklogger/consumer-rules.pro new file mode 100644 index 0000000..e69de29 diff --git a/tasklogger/proguard-rules.pro b/tasklogger/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/tasklogger/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/tasklogger/src/androidTest/java/com/phonereplay/tasklogger/ExampleInstrumentedTest.java b/tasklogger/src/androidTest/java/com/phonereplay/tasklogger/ExampleInstrumentedTest.java new file mode 100644 index 0000000..4596dab --- /dev/null +++ b/tasklogger/src/androidTest/java/com/phonereplay/tasklogger/ExampleInstrumentedTest.java @@ -0,0 +1,26 @@ +package com.phonereplay.tasklogger; + +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.phonereplay.tasklogger.test", appContext.getPackageName()); + } +} \ No newline at end of file diff --git a/tasklogger/src/main/AndroidManifest.xml b/tasklogger/src/main/AndroidManifest.xml new file mode 100644 index 0000000..a5918e6 --- /dev/null +++ b/tasklogger/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/tasklogger/src/main/java/com/phonereplay/tasklogger/DeviceInfo.java b/tasklogger/src/main/java/com/phonereplay/tasklogger/DeviceInfo.java new file mode 100644 index 0000000..12fcd06 --- /dev/null +++ b/tasklogger/src/main/java/com/phonereplay/tasklogger/DeviceInfo.java @@ -0,0 +1,83 @@ +package com.phonereplay.tasklogger; + +import android.annotation.SuppressLint; +import android.app.ActivityManager; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; +import android.os.BatteryManager; +import android.os.Environment; +import android.os.StatFs; +import android.util.DisplayMetrics; + +import java.util.Locale; + +public class DeviceInfo { + + public static String getTotalStorage() { + StatFs statFs = new StatFs(Environment.getExternalStorageDirectory().getPath()); + long bytesAvailable = (long) statFs.getBlockSize() * (long) statFs.getBlockCount(); + return formatSize(bytesAvailable); + } + + + public static String getTotalRAM(Context context) { + ActivityManager activityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); + ActivityManager.MemoryInfo memoryInfo = new ActivityManager.MemoryInfo(); + activityManager.getMemoryInfo(memoryInfo); + long totalMemory = memoryInfo.totalMem; + return formatSize(totalMemory); + } + + + public static String getCurrentNetwork(Context context) { + ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); + NetworkInfo activeNetwork = cm.getActiveNetworkInfo(); + if (activeNetwork != null) { + if (activeNetwork.getType() == ConnectivityManager.TYPE_WIFI) { + return "WIFI"; + } else if (activeNetwork.getType() == ConnectivityManager.TYPE_MOBILE) { + return "MOBILE"; + } + } + return "NOT CONNECTED"; + } + + public static String getLanguage() { + return Locale.getDefault().getDisplayLanguage(); + } + + public static float getBatteryLevel(Context context) { + IntentFilter ifilter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED); + Intent batteryStatus = context.registerReceiver(null, ifilter); + int level = batteryStatus != null ? batteryStatus.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) : -1; + int scale = batteryStatus != null ? batteryStatus.getIntExtra(BatteryManager.EXTRA_SCALE, -1) : -1; + return level / (float) scale * 100; + } + + @SuppressLint("DefaultLocale") + private static String formatSize(long bytes) { + long kilobyte = 1024; + long megabyte = kilobyte * 1024; + long gigabyte = megabyte * 1024; + + if (bytes >= gigabyte) { + return String.format("%.2f GB", (float) bytes / gigabyte); + } else if (bytes >= megabyte) { + return String.format("%.2f MB", (float) bytes / megabyte); + } else if (bytes >= kilobyte) { + return String.format("%.2f KB", (float) bytes / kilobyte); + } else { + return String.format("%d Bytes", bytes); + } + } + + public static String getScreenResolution(Context context) { + DisplayMetrics metrics = context.getResources().getDisplayMetrics(); + int width = metrics.widthPixels; + int height = metrics.heightPixels; + return width + "x" + height; + } +} diff --git a/tasklogger/src/main/java/com/phonereplay/tasklogger/DeviceModel.java b/tasklogger/src/main/java/com/phonereplay/tasklogger/DeviceModel.java new file mode 100644 index 0000000..73cdbfb --- /dev/null +++ b/tasklogger/src/main/java/com/phonereplay/tasklogger/DeviceModel.java @@ -0,0 +1,42 @@ +package com.phonereplay.tasklogger; + +import android.content.Context; +import android.os.Build; + + +public class DeviceModel { + + private final String manufacturer; + private final String model; + private final String device; + private final String brand; + private final String osVersion; + private final int sdkVersion; + private final String installID; + private final String totalStorage; + private final String totalRAM; + private final String currentNetwork; + private final String language; + private final float batteryLevel; + private final String sessionId; + private final String screenResolution; + private final String platform; + + public DeviceModel(Context context, String sessionId) { + this.sessionId = sessionId; + this.manufacturer = Build.MANUFACTURER; + this.model = Build.MODEL; + this.device = Build.DEVICE; + this.brand = Build.BRAND; + this.osVersion = Build.VERSION.RELEASE; + this.sdkVersion = Build.VERSION.SDK_INT; + this.installID = Build.ID; + this.totalStorage = DeviceInfo.getTotalStorage(); + this.totalRAM = DeviceInfo.getTotalRAM(context); // Asumindo que esta lógica é implementada em DeviceInfo + this.currentNetwork = DeviceInfo.getCurrentNetwork(context); // Asumindo que esta lógica é implementada em DeviceInfo + this.language = DeviceInfo.getLanguage(); + this.batteryLevel = DeviceInfo.getBatteryLevel(context); // Asumindo que esta lógica é implementada em DeviceInfo + this.screenResolution = DeviceInfo.getScreenResolution(context); + this.platform = "Android"; + } +} diff --git a/tasklogger/src/main/java/com/phonereplay/tasklogger/GestureRecorder.java b/tasklogger/src/main/java/com/phonereplay/tasklogger/GestureRecorder.java new file mode 100644 index 0000000..935f165 --- /dev/null +++ b/tasklogger/src/main/java/com/phonereplay/tasklogger/GestureRecorder.java @@ -0,0 +1,56 @@ +package com.phonereplay.tasklogger; + +import android.os.Build; + +import com.phonereplay.tasklogger.service.PhoneReplayService; + +import java.util.Optional; + +public class GestureRecorder { + public final LocalSession currentSession; + private final PhoneReplayService phoneReplayService; + + public GestureRecorder(String sessionId, String projectId, PhoneReplayService phoneReplayService) { + this.phoneReplayService = phoneReplayService; + this.currentSession = new LocalSession(sessionId, projectId); + } + + public String generateSummaryLog() { + StringBuilder logBuilder = new StringBuilder(); + logBuilder.append("Session ID: ").append(currentSession.id).append("\n"); + for (LocalActivity activity : currentSession.activities) { + logBuilder.append("Activity ID: ").append(activity.id).append("\n"); + for (LocalGesture gesture : activity.gestures) { + logBuilder.append("\tGesture: ").append(gesture.gestureType) + .append(" at Time: ").append(gesture.targetTime) + .append(", horário: ").append(gesture.createdAt).append("\n"); + } + } + return logBuilder.toString(); + } + + public void sendLocalSessionData() { + phoneReplayService.sendLocalSessionData(currentSession); + } + + public void registerGesture(String activityName, String gestureType, String targetTime, String coordinates) { + LocalActivity activity = findOrCreateActivity(activityName); + assert activity != null; + LocalGesture gesture = new LocalGesture(activity.id, gestureType, targetTime, coordinates); + activity.addGesture(gesture); + } + + private LocalActivity findOrCreateActivity(String activityName) { + Optional existingActivity = currentSession.getActivity(activityName); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + if (existingActivity.isPresent()) { + return existingActivity.get(); + } else { + LocalActivity newActivity = new LocalActivity(activityName); + currentSession.addActivity(newActivity); + return newActivity; + } + } + return null; + } +} diff --git a/tasklogger/src/main/java/com/phonereplay/tasklogger/LocalActivity.java b/tasklogger/src/main/java/com/phonereplay/tasklogger/LocalActivity.java new file mode 100644 index 0000000..6807309 --- /dev/null +++ b/tasklogger/src/main/java/com/phonereplay/tasklogger/LocalActivity.java @@ -0,0 +1,28 @@ +package com.phonereplay.tasklogger; + +import com.google.gson.annotations.SerializedName; + +import java.util.HashSet; +import java.util.Set; + +public class LocalActivity { + @SerializedName("id") + String id; + + @SerializedName("activityName") + String activityName; + + @SerializedName("gestures") + Set gestures; + + public LocalActivity(String id) { + this.id = id; + this.activityName = id; + this.gestures = new HashSet<>(); + } + + public void addGesture(LocalGesture gesture) { + this.gestures.add(gesture); + } +} + diff --git a/tasklogger/src/main/java/com/phonereplay/tasklogger/LocalGesture.java b/tasklogger/src/main/java/com/phonereplay/tasklogger/LocalGesture.java new file mode 100644 index 0000000..22aacfd --- /dev/null +++ b/tasklogger/src/main/java/com/phonereplay/tasklogger/LocalGesture.java @@ -0,0 +1,35 @@ +package com.phonereplay.tasklogger; + +import android.annotation.SuppressLint; + +import com.google.gson.annotations.SerializedName; + +import java.text.SimpleDateFormat; +import java.util.Date; + +public class LocalGesture { + @SerializedName("activityId") + public String activityId; + + @SerializedName("gestureType") + public String gestureType; + + @SerializedName("targetTime") + public String targetTime; + @SerializedName("createdAt") + public String createdAt; // Horário em que o gesto foi registrado + + @SerializedName("coordinates") + public String coordinates; + + // Construtor atualizado para aceitar targetTime diretamente + @SuppressLint("SimpleDateFormat") + public LocalGesture(String activityId, String gestureType, String targetTime, String coordinates) { + this.activityId = activityId; + this.gestureType = gestureType; + this.targetTime = targetTime; + this.createdAt = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()); + this.coordinates = coordinates; + } +} + diff --git a/tasklogger/src/main/java/com/phonereplay/tasklogger/LocalSession.java b/tasklogger/src/main/java/com/phonereplay/tasklogger/LocalSession.java new file mode 100644 index 0000000..cd7eec8 --- /dev/null +++ b/tasklogger/src/main/java/com/phonereplay/tasklogger/LocalSession.java @@ -0,0 +1,44 @@ +package com.phonereplay.tasklogger; + +import android.os.Build; + +import java.util.HashSet; +import java.util.Optional; +import java.util.Set; + +public class LocalSession { + String id; + String projectId; + Set activities; + + public LocalSession(String id, String projectId) { + this.id = id; + this.projectId = projectId; + this.activities = new HashSet<>(); + } + + public void addActivity(LocalActivity activity) { + this.activities.add(activity); + } + + public Optional getActivity(String activityId) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + return this.activities.stream() + .filter(activity -> activity.id.equals(activityId)) + .findFirst(); + } + return null; + } + + // Método para recuperar TimeLines de todas as atividades + public Set getTimeLines() { + Set timeLines = new HashSet<>(); + for (LocalActivity activity : activities) { + for (LocalGesture gesture : activity.gestures) { + TimeLine timeLine = new TimeLine(gesture.coordinates, gesture.gestureType, gesture.targetTime); + timeLines.add(timeLine); + } + } + return timeLines; + } +} diff --git a/tasklogger/src/main/java/com/phonereplay/tasklogger/PhoneReplay.java b/tasklogger/src/main/java/com/phonereplay/tasklogger/PhoneReplay.java new file mode 100644 index 0000000..2c36af8 --- /dev/null +++ b/tasklogger/src/main/java/com/phonereplay/tasklogger/PhoneReplay.java @@ -0,0 +1,188 @@ +package com.phonereplay.tasklogger; + +import android.annotation.SuppressLint; +import android.app.Activity; +import android.app.Instrumentation; +import android.content.Context; +import android.content.ContextWrapper; +import android.content.Intent; +import android.os.Bundle; +import android.util.DisplayMetrics; +import android.util.Log; +import android.view.Window; + +import com.phonereplay.tasklogger.reflect.Reflect; + +public class PhoneReplay extends Activity { + + @SuppressLint("StaticFieldLeak") + private static PhoneReplayApi phoneReplayApi; + @SuppressLint("StaticFieldLeak") + private static PhoneReplay sInstance; + private final Context mAppContext; + private Activity currentActivity; + + private int previousWidth; + private int previousHeight; + + public PhoneReplay(Context appContext) { + mAppContext = appContext; + phoneReplayApi = new PhoneReplayApi(appContext); + } + + public static void init(Context application, String accessKey) { + PhoneReplay.getInstance(application).attachBaseContext(); + new Thread(() -> phoneReplayApi.getApiClientService().verifyProjectAuth(accessKey)).start(); + } + + public synchronized static PhoneReplay getInstance(final Context appContext) { + if (sInstance == null) { + sInstance = new PhoneReplay(appContext); + } + return sInstance; + } + + private void updateDimensions(int width, int height) { + if (previousWidth != 0) { + phoneReplayApi.orientation = true; + } + previousWidth = width; + previousHeight = height; + } + + public Activity getCurrentActivity() { + return currentActivity; + } + + public void setCurrentActivity(Activity activity) { + this.currentActivity = activity; + PhoneReplayApi.setCurrentActivity(activity); // Adicione esta linha + } + + private void replaceInstrumentation(Context contextImpl) { + Reflect contextImplRef = Reflect.on(contextImpl); + Reflect activityThreadRef = contextImplRef.field("mMainThread"); + Reflect instrumentationRef = activityThreadRef.field("mInstrumentation"); + TaskLoggerInstrumentation newInstrumentation = new TaskLoggerInstrumentation(instrumentationRef.get()); + activityThreadRef.set("mInstrumentation", newInstrumentation); + } + + private void attachBaseContext() { + phoneReplayApi.initThread(); + phoneReplayApi.initHandler(); + Context contextImpl = getContextImpl(mAppContext); + replaceInstrumentation(contextImpl); + } + + private Context getContextImpl(Context context) { + Context nextContext; + while ((context instanceof ContextWrapper) && + (nextContext = ((ContextWrapper) context).getBaseContext()) != null) { + context = nextContext; + } + return context; + } + + private class TaskLoggerInstrumentation extends Instrumentation { + + Instrumentation base; + Reflect instrumentRef; + + public TaskLoggerInstrumentation(Instrumentation base) { + this.base = base; + instrumentRef = Reflect.on(base); + } + + private void initThread(Activity activity) { + if (!activity.equals(getCurrentActivity())) { + phoneReplayApi.getmHandler().removeCallbacks(phoneReplayApi.getThread()); + phoneReplayApi.getmHandler().postDelayed(phoneReplayApi.getThread(), 100); + setCurrentActivity(activity); + } + initView(activity); + } + + private void initView(Activity activity) { + phoneReplayApi.setCurrentView(activity.getWindow().getDecorView()); + phoneReplayApi.getCurrentView().setDrawingCacheEnabled(true); + } + + @Override + public void callActivityOnCreate(Activity activity, Bundle bundle) { + initThread(activity); + super.callActivityOnCreate(activity, bundle); + } + + @Override + public void callActivityOnNewIntent(Activity activity, Intent intent) { + super.callActivityOnNewIntent(activity, intent); + } + + @Override + public void callActivityOnRestart(Activity activity) { + super.callActivityOnRestart(activity); + } + + @Override + public void callActivityOnStart(Activity activity) { + initThread(activity); + Window window = activity.getWindow(); + + if (window != null && !(window.getCallback() instanceof UserInteractionAwareCallback)) { + window.setCallback(new UserInteractionAwareCallback(window.getCallback(), activity)); + } + super.callActivityOnStart(activity); + } + + /** + * Method to update and handle screen dimensions. + * + * @param activity The current Activity context to get display metrics. + */ + public void updateAndHandleScreenDimensions(Activity activity) { + DisplayMetrics displayMetrics = new DisplayMetrics(); + // Use activity's context to get the WindowManager + activity.getWindowManager().getDefaultDisplay().getMetrics(displayMetrics); + phoneReplayApi.setMainHeight(displayMetrics.heightPixels, displayMetrics.widthPixels); + // Check if dimensions have changed + if (displayMetrics.widthPixels != previousWidth || displayMetrics.heightPixels != previousHeight) { + // Dimensions have changed, do something here + updateDimensions(displayMetrics.widthPixels, displayMetrics.heightPixels); // Update stored dimensions + // Add your logic to handle the resolution change + } + Log.d("YourClassName", "Screen width: " + displayMetrics.widthPixels + ", height: " + displayMetrics.heightPixels); + } + + + @Override + public void callActivityOnResume(Activity activity) { + Log.d("Instrumentation", "OnResume"); + updateAndHandleScreenDimensions(activity); + super.callActivityOnResume(activity); + } + + @Override + public void callActivityOnPause(Activity activity) { + Log.d("Instrumentation", "OnPause"); + super.callActivityOnPause(activity); + } + + @Override + public void callActivityOnStop(Activity activity) { + Log.d("Instrumentation", "OnStop"); + super.callActivityOnStop(activity); + } + + @Override + public void callActivityOnDestroy(Activity activity) { + Log.d("Instrumentation", "OnDestroy"); + super.callActivityOnDestroy(activity); + } + + @Override + public void callActivityOnUserLeaving(Activity activity) { + Log.d("Instrumentation", "OnUserLeaving"); + super.callActivityOnUserLeaving(activity); + } + } +} diff --git a/tasklogger/src/main/java/com/phonereplay/tasklogger/PhoneReplayApi.java b/tasklogger/src/main/java/com/phonereplay/tasklogger/PhoneReplayApi.java new file mode 100644 index 0000000..35aa57d --- /dev/null +++ b/tasklogger/src/main/java/com/phonereplay/tasklogger/PhoneReplayApi.java @@ -0,0 +1,203 @@ +package com.phonereplay.tasklogger; + +import android.annotation.SuppressLint; +import android.app.Activity; +import android.content.Context; +import android.graphics.Bitmap; +import android.os.Handler; +import android.util.Log; +import android.view.View; +import android.widget.Toast; + +import com.phonereplay.tasklogger.exception.MyExceptionHandler; +import com.phonereplay.tasklogger.network.models.Project; +import com.phonereplay.tasklogger.network.models.reponses.CreateSessionResponse; +import com.phonereplay.tasklogger.service.PhoneReplayService; +import com.phonereplay.tasklogger.utils.BitmapUtils; + +import java.io.IOException; + +public class PhoneReplayApi { + + /** + * Intervalo de captura em milissegundos. + * Certifique-se de que o intervalo de captura e a taxa de quadros do vídeo (SERVIDOR PYTHON) estão alinhados. + * Se você está capturando a cada 100 milliseconds, isso seria aproximadamente 10 quadros por segundo + * (considerando que 1 segundo = 1000 milliseconds). + */ + private static final int RECORDING_INTERVAL = 100; + public static boolean startRecording = false; + private static Thread thread; + private static Handler mHandler; + @SuppressLint("StaticFieldLeak") + private static PhoneReplayService apiClientService; + @SuppressLint("StaticFieldLeak") + private static Activity currentActivity; + @SuppressLint("StaticFieldLeak") + private static GestureRecorder gestureRecorder; + private static StopwatchUtility stopwatch = new StopwatchUtility(); + @SuppressLint("StaticFieldLeak") + private static Context context; + public boolean orientation = false; + public int mainHeight = 0; + public int mainWidth = 0; + private View currentView; + + public PhoneReplayApi(Context context) { + apiClientService = new PhoneReplayService(context); + Thread.setDefaultUncaughtExceptionHandler(new MyExceptionHandler(context)); + PhoneReplayApi.context = context; + } + + public static StopwatchUtility getStopwatch() { + if (stopwatch == null) { + stopwatch = new StopwatchUtility(); + } + return stopwatch; + } + + + public static Context getContext() { + return context; + } + + public static void setCurrentActivity(Activity activity) { + currentActivity = activity; + } + + public static void startRecording() { + Toast.makeText(context, "DEV TEST: gravando video", Toast.LENGTH_LONG).show(); + + if (apiClientService.getVerifyProjectAuthResponse() == null) { + new Thread(() -> apiClientService.verifyProjectAuth()).start(); + } + if (apiClientService.getVerifyProjectAuthResponse().isAuth()) { + + Project project = apiClientService.getVerifyProjectAuthResponse().getProject(); + + new Thread(() -> { + CreateSessionResponse createSessionResponseResponse = apiClientService.createSession(project.getProjectId()); + if (createSessionResponseResponse == null) { + return; + } + if (createSessionResponseResponse.isCreated()) { + String projectId = apiClientService.getVerifyProjectAuthResponse().getProject().getProjectId(); + gestureRecorder = new GestureRecorder(createSessionResponseResponse.getSession().getSessionId(), projectId, apiClientService); + + startRecording = true; + mHandler.postDelayed(thread, RECORDING_INTERVAL); + startCountUp(); + } + }).start(); + } + } + + public static void stopRecording() { + startRecording = false; + mHandler.removeCallbacks(thread); + Log.d("timer", stopwatch.timer); + stopwatch.stop(); + + Toast.makeText(context, "DEV TEST: parando gravacao de video", Toast.LENGTH_LONG).show(); + + if (apiClientService.getVerifyProjectAuthResponse() == null) { + return; + } + if (apiClientService.getVerifyProjectAuthResponse().isAuth()) { + if (gestureRecorder != null) { + String summaryLog = gestureRecorder.generateSummaryLog(); + Log.d("GestureRecorderSummary", summaryLog); + gestureRecorder.sendLocalSessionData(); + DeviceModel deviceModel = new DeviceModel(context, apiClientService.getSession().getSession().getSessionId()); + apiClientService.sendDeviceInfo(deviceModel); + } + new Thread(() -> { + try { + apiClientService.createVideo(apiClientService.getSession().getSession().getSessionId(), gestureRecorder.currentSession.getTimeLines()); + } catch (IOException e) { + throw new RuntimeException(e); + } + }).start(); + } + } + + private static void startCountUp() { + if (stopwatch == null) { + stopwatch = new StopwatchUtility(); + } + stopwatch.start(); + } + + public static void registerTouchAction(String action, float x, float y) { + if (startRecording && currentActivity != null) { + String activityName = currentActivity.getClass().getSimpleName(); + String targetTime = stopwatch.timer; + String gestureType = "(" + x + ", " + y + ")"; + gestureRecorder.registerGesture(activityName, action, targetTime, gestureType); + Log.d("ActionRegistered", "Gesture: " + gestureType + ", Time: " + targetTime); + } + } + + public void setMainHeight(int mainHeight, int mainWidth) { + if (this.mainHeight == 0) { + this.mainHeight = mainHeight; + this.mainWidth = mainWidth; + } + } + + public PhoneReplayService getApiClientService() { + return apiClientService; + } + + public View getCurrentView() { + return currentView; + } + + public void setCurrentView(View currentView) { + this.currentView = currentView; + } + + public Handler getmHandler() { + return mHandler; + } + + public Thread getThread() { + return thread; + } + + public void initHandler() { + mHandler = new Handler(); + } + + + public void initThread() { + thread = new Thread() { + @Override + public void run() { + try { + new Thread() { + @Override + public void run() { + if (startRecording) { + try { + Bitmap bitmap = BitmapUtils.convertViewToDrawable(currentView); + apiClientService.queueBytesBitmapV2(bitmap); + currentView.destroyDrawingCache(); + } catch (Exception e) { + e.printStackTrace(); + } + super.run(); + } + } + }.start(); + if (startRecording) { + mHandler.postDelayed(thread, RECORDING_INTERVAL); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + }; + } +} + diff --git a/tasklogger/src/main/java/com/phonereplay/tasklogger/PhoneReplayApplication.java b/tasklogger/src/main/java/com/phonereplay/tasklogger/PhoneReplayApplication.java new file mode 100644 index 0000000..b95dd60 --- /dev/null +++ b/tasklogger/src/main/java/com/phonereplay/tasklogger/PhoneReplayApplication.java @@ -0,0 +1,12 @@ +package com.phonereplay.tasklogger; + +import android.app.Application; +import android.content.Context; + +public class PhoneReplayApplication extends Application { + @Override + protected void attachBaseContext(Context base) { + super.attachBaseContext(base); + PhoneReplay.init(this, "6e55b630-88a2-4545-be55-65bd68972d6b"); + } +} diff --git a/tasklogger/src/main/java/com/phonereplay/tasklogger/StopwatchUtility.java b/tasklogger/src/main/java/com/phonereplay/tasklogger/StopwatchUtility.java new file mode 100644 index 0000000..1c89e20 --- /dev/null +++ b/tasklogger/src/main/java/com/phonereplay/tasklogger/StopwatchUtility.java @@ -0,0 +1,84 @@ +package com.phonereplay.tasklogger; + +import android.os.Handler; +import android.os.Looper; +import android.os.SystemClock; +import android.util.Log; + +public class StopwatchUtility { + private final Handler handler; + public String timer; + private int hours, minutes, seconds, milliSeconds; + private long millisecondTime, startTime, timeBuff, updateTime = 0L; + private boolean isRunning = false; + private StopwatchListener listener; + private final Runnable runnable = new Runnable() { + @Override + public void run() { + millisecondTime = SystemClock.uptimeMillis() - startTime; + updateTime = timeBuff + millisecondTime; + + hours = (int) (updateTime / (3600 * 1000)); + minutes = (int) (updateTime / (60 * 1000)) % 60; + seconds = (int) (updateTime / 1000) % 60; + milliSeconds = (int) (updateTime % 1000); + isRunning = true; + + onTick(hours, minutes, seconds, milliSeconds); + + handler.postDelayed(this, 0); + } + }; + + public StopwatchUtility() { + handler = new Handler(Looper.getMainLooper()); + } + + public void start() { + if (!isRunning) { + startTime = SystemClock.uptimeMillis(); + handler.postDelayed(runnable, 0); + isRunning = true; + } + } + + public void stop() { + if (isRunning) { + timeBuff += millisecondTime; + handler.removeCallbacks(runnable); + isRunning = false; + } + reset(); + } + + private void reset() { + millisecondTime = 0L; + startTime = 0L; + timeBuff = 0L; + updateTime = 0L; + hours = 0; + minutes = 0; + seconds = 0; + milliSeconds = 0; + timer = "00:00:00.00"; + handler.removeCallbacks(runnable); + isRunning = false; + } + + public void setStopwatchListener(StopwatchListener listener) { + this.listener = listener; + } + + private void onTick(int hours, int minutes, int seconds, int milliSeconds) { + double fractionalSeconds = seconds + (milliSeconds / 1000.0); + timer = String.format("%02d:%02d:%05.2f", hours, minutes, fractionalSeconds); + Log.d("onTick", "onTick: " + timer); + if (listener != null) { + listener.onTick(timer); + } + } + + public interface StopwatchListener { + void onTick(String time); + } +} diff --git a/tasklogger/src/main/java/com/phonereplay/tasklogger/TimeLine.java b/tasklogger/src/main/java/com/phonereplay/tasklogger/TimeLine.java new file mode 100644 index 0000000..d9dddb4 --- /dev/null +++ b/tasklogger/src/main/java/com/phonereplay/tasklogger/TimeLine.java @@ -0,0 +1,28 @@ +package com.phonereplay.tasklogger; + +public class TimeLine { + private final String coordinates; + private final String gestureType; + private final String targetTime; + + public TimeLine(String coordinates, String gestureType, String targetTime) { + this.coordinates = coordinates; + this.gestureType = gestureType; + this.targetTime = targetTime; + } + + // Getters + public String getCoordinates() { + return coordinates; + } + + public String getGestureType() { + return gestureType; + } + + public String getTargetTime() { + return targetTime; + } + + // Opcional: Setters, toString(), etc. +} diff --git a/tasklogger/src/main/java/com/phonereplay/tasklogger/UserInteractionAwareCallback.java b/tasklogger/src/main/java/com/phonereplay/tasklogger/UserInteractionAwareCallback.java new file mode 100644 index 0000000..26f1d58 --- /dev/null +++ b/tasklogger/src/main/java/com/phonereplay/tasklogger/UserInteractionAwareCallback.java @@ -0,0 +1,198 @@ +package com.phonereplay.tasklogger; + +import android.app.Activity; +import android.view.ActionMode; +import android.view.GestureDetector; +import android.view.KeyEvent; +import android.view.Menu; +import android.view.MenuItem; +import android.view.MotionEvent; +import android.view.SearchEvent; +import android.view.View; +import android.view.Window.Callback; +import android.view.WindowManager; +import android.view.accessibility.AccessibilityEvent; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +public class UserInteractionAwareCallback extends GestureDetector.SimpleOnGestureListener implements Callback { + + private static final long SWIPE_THRESHOLD = 100; + + private static final long SWIPE_VELOCITY_THRESHOLD = 100; + + private final Callback originalCallback; + + private final Activity activity; + + + private final GestureDetector gestureDetector; + + + public UserInteractionAwareCallback(final Callback originalCallback, final Activity activity) { + this.originalCallback = originalCallback; + this.activity = activity; + this.gestureDetector = new GestureDetector(activity, this); + } + + @Override + public boolean dispatchTouchEvent(final MotionEvent event) { + gestureDetector.onTouchEvent(event); + return originalCallback.dispatchTouchEvent(event); + } + + @Override + public boolean onDown(@NonNull MotionEvent e) { + return true; + } + + @Override + public boolean onSingleTapUp(MotionEvent e) { + handleTouchAction("Single Tap", e.getX(), e.getY()); + return true; + } + + @Override + public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { + assert e1 != null && e2 != null; // Assegura que e1 e e2 não são nulos + float diffY = e2.getY() - e1.getY(); + float diffX = e2.getX() - e1.getX(); + if (Math.abs(diffX) < Math.abs(diffY)) { + if (Math.abs(diffY) > SWIPE_THRESHOLD && Math.abs(velocityY) > SWIPE_VELOCITY_THRESHOLD) { + if (diffY > 0) { + handleTouchAction("Swipe Down", e2.getX(), e2.getY()); + } else { + handleTouchAction("Swipe Up", e2.getX(), e2.getY()); + } + return true; + } + } + return false; + } + + @Override + public boolean onScroll(MotionEvent e1, @NonNull MotionEvent e2, float distanceX, float distanceY) { + //handleTouchAction("Trail", e1.getEventTime()); + return true; + } + + private void handleTouchAction(String action, float x, float y) { + PhoneReplayApi.registerTouchAction(action, x, y); + if (activity != null) { + activity.onUserInteraction(); + } + } + + @Override + public boolean dispatchKeyEvent(KeyEvent event) { + return originalCallback.dispatchKeyEvent(event); + } + + @Override + public boolean dispatchKeyShortcutEvent(KeyEvent event) { + return originalCallback.dispatchKeyShortcutEvent(event); + } + + @Override + public boolean dispatchTrackballEvent(MotionEvent event) { + return originalCallback.dispatchTrackballEvent(event); + } + + @Override + public boolean dispatchGenericMotionEvent(MotionEvent event) { + return originalCallback.dispatchGenericMotionEvent(event); + } + + @Override + public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { + return originalCallback.dispatchPopulateAccessibilityEvent(event); + } + + @Override + public View onCreatePanelView(int featureId) { + return originalCallback.onCreatePanelView(featureId); + } + + @Override + public boolean onCreatePanelMenu(int featureId, @NonNull Menu menu) { + return originalCallback.onCreatePanelMenu(featureId, menu); + } + + @Override + public boolean onPreparePanel(int featureId, View view, @NonNull Menu menu) { + return originalCallback.onPreparePanel(featureId, view, menu); + } + + @Override + public boolean onMenuOpened(int featureId, @NonNull Menu menu) { + return originalCallback.onMenuOpened(featureId, menu); + } + + @Override + public boolean onMenuItemSelected(int featureId, @NonNull MenuItem item) { + return originalCallback.onMenuItemSelected(featureId, item); + } + + @Override + public void onWindowAttributesChanged(WindowManager.LayoutParams attrs) { + originalCallback.onWindowAttributesChanged(attrs); + } + + @Override + public void onContentChanged() { + originalCallback.onContentChanged(); + } + + @Override + public void onWindowFocusChanged(boolean hasFocus) { + originalCallback.onWindowFocusChanged(hasFocus); + } + + @Override + public void onAttachedToWindow() { + originalCallback.onAttachedToWindow(); + } + + @Override + public void onDetachedFromWindow() { + originalCallback.onDetachedFromWindow(); + } + + @Override + public void onPanelClosed(int featureId, @NonNull Menu menu) { + originalCallback.onPanelClosed(featureId, menu); + } + + @Override + public boolean onSearchRequested() { + return originalCallback.onSearchRequested(); + } + + @Override + public boolean onSearchRequested(SearchEvent searchEvent) { + return false; + } + + @Nullable + @Override + public ActionMode onWindowStartingActionMode(ActionMode.Callback callback) { + return null; + } + + @Nullable + @Override + public ActionMode onWindowStartingActionMode(ActionMode.Callback callback, int type) { + return null; + } + + @Override + public void onActionModeStarted(ActionMode mode) { + + } + + @Override + public void onActionModeFinished(ActionMode mode) { + + } +} \ No newline at end of file diff --git a/tasklogger/src/main/java/com/phonereplay/tasklogger/exception/MyExceptionHandler.java b/tasklogger/src/main/java/com/phonereplay/tasklogger/exception/MyExceptionHandler.java new file mode 100644 index 0000000..5f09ddc --- /dev/null +++ b/tasklogger/src/main/java/com/phonereplay/tasklogger/exception/MyExceptionHandler.java @@ -0,0 +1,25 @@ +package com.phonereplay.tasklogger.exception; + +import android.content.Context; + +public class MyExceptionHandler implements Thread.UncaughtExceptionHandler { + private final Context context; + private final Thread.UncaughtExceptionHandler defaultUEH; + + public MyExceptionHandler(Context context) { + this.context = context; + this.defaultUEH = Thread.getDefaultUncaughtExceptionHandler(); // Mantenha o manipulador padrão + } + + @Override + public void uncaughtException(Thread thread, Throwable ex) { + ex.printStackTrace(); // Loga a exceção + + // Reenvie para o manipulador padrão + if (defaultUEH != null) { + defaultUEH.uncaughtException(thread, ex); + } else { + System.exit(2); // Encerra o aplicativo + } + } +} diff --git a/tasklogger/src/main/java/com/phonereplay/tasklogger/network/ApiClient.java b/tasklogger/src/main/java/com/phonereplay/tasklogger/network/ApiClient.java new file mode 100644 index 0000000..8e095f5 --- /dev/null +++ b/tasklogger/src/main/java/com/phonereplay/tasklogger/network/ApiClient.java @@ -0,0 +1,62 @@ +package com.phonereplay.tasklogger.network; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.phonereplay.tasklogger.DeviceModel; +import com.phonereplay.tasklogger.LocalSession; +import com.phonereplay.tasklogger.network.models.reponses.CreateSessionResponse; +import com.phonereplay.tasklogger.network.models.reponses.VerifyProjectAuthResponse; + +import retrofit2.Call; +import retrofit2.Retrofit; +import retrofit2.converter.gson.GsonConverterFactory; + +public class ApiClient implements PhoneReplayApiInterface { + + public static Retrofit retrofit = null; + + public ApiClient() { + getClient(); + } + + public void getClient() { + Gson gson = new GsonBuilder() + .setDateFormat("yyyy-MM-dd'T'HH:mm:ssZ") + .create(); + + retrofit = new Retrofit.Builder() + .baseUrl("http://10.0.0.100:3000") + .addConverterFactory(GsonConverterFactory.create(gson)) + .build(); + } + + public PhoneReplayApiInterface getInstance() { + return retrofit.create(PhoneReplayApiInterface.class); + } + + @Override + public Call createSession(String projectId) { + return getInstance().createSession(projectId); + } + + @Override + public Call verifyProjectAuth(String project_access_key) { + return getInstance().verifyProjectAuth(project_access_key); + } + + @Override + public Call createVideo(String sessionId) { + return getInstance().createVideo(sessionId); + } + + @Override + public Call sendLocalSessionData(LocalSession localSession) { + return getInstance().sendLocalSessionData(localSession); + + } + + @Override + public Call sendDeviceInfo(DeviceModel deviceModel) { + return getInstance().sendDeviceInfo(deviceModel); + } +} \ No newline at end of file diff --git a/tasklogger/src/main/java/com/phonereplay/tasklogger/network/GrpcClient.java b/tasklogger/src/main/java/com/phonereplay/tasklogger/network/GrpcClient.java new file mode 100644 index 0000000..af755b3 --- /dev/null +++ b/tasklogger/src/main/java/com/phonereplay/tasklogger/network/GrpcClient.java @@ -0,0 +1,56 @@ +package com.phonereplay.tasklogger.network; + +import com.google.protobuf.ByteString; +import com.phonereplay.tasklogger.Binary; +import com.phonereplay.tasklogger.BinaryDataServiceGrpc; +import com.phonereplay.tasklogger.TimeLine; + +import java.util.Set; + +import io.grpc.ManagedChannel; +import io.grpc.ManagedChannelBuilder; + +public class GrpcClient { + + public void sendBinaryData(byte[] binaryData, String sessionId, Set timeLines) { + if (binaryData == null || sessionId == null) { + return; + } + String serverAddress = "10.0.0.100"; //you ip + int serverPort = 8000; + + ManagedChannel channel = ManagedChannelBuilder.forAddress(serverAddress, serverPort) + .usePlaintext() + .maxInboundMessageSize(50 * 1024 * 1024) + .build(); + + try { + BinaryDataServiceGrpc.BinaryDataServiceBlockingStub blockingStub = BinaryDataServiceGrpc.newBlockingStub(channel); + + Binary.BinaryDataRequest.Builder requestBuilder = Binary.BinaryDataRequest.newBuilder() + .setBinaryData(ByteString.copyFrom(binaryData)) + .setSessionId(sessionId); + + // Adicionando TimeLines à requisição + for (TimeLine tl : timeLines) { + Binary.TimeLine.Builder tlBuilder = Binary.TimeLine.newBuilder() + .setCoordinates(tl.getCoordinates()) + .setGestureType(tl.getGestureType()) + .setTargetTime(tl.getTargetTime()); + requestBuilder.addTimeLines(tlBuilder); + } + + Binary.BinaryDataRequest request = requestBuilder.build(); + Binary.BinaryDataResponse response = blockingStub.sendBinaryData(request); + + System.out.println("Response from server: " + response); + + } catch (Exception e) { + e.printStackTrace(); + } finally { + if (channel != null && !channel.isShutdown()) { + channel.shutdown(); + } + } + } +} diff --git a/tasklogger/src/main/java/com/phonereplay/tasklogger/network/PhoneReplayApiInterface.java b/tasklogger/src/main/java/com/phonereplay/tasklogger/network/PhoneReplayApiInterface.java new file mode 100644 index 0000000..f3ea313 --- /dev/null +++ b/tasklogger/src/main/java/com/phonereplay/tasklogger/network/PhoneReplayApiInterface.java @@ -0,0 +1,30 @@ +package com.phonereplay.tasklogger.network; + +import com.phonereplay.tasklogger.DeviceModel; +import com.phonereplay.tasklogger.LocalSession; +import com.phonereplay.tasklogger.network.models.reponses.CreateSessionResponse; +import com.phonereplay.tasklogger.network.models.reponses.VerifyProjectAuthResponse; + +import retrofit2.Call; +import retrofit2.http.Body; +import retrofit2.http.GET; +import retrofit2.http.POST; +import retrofit2.http.Path; + +public interface PhoneReplayApiInterface { + + @POST("api/session/create/{project_id}") + Call createSession(@Path("project_id") String projectId); + + @GET("api/project/check-project/{project_access_key}") + Call verifyProjectAuth(@Path("project_access_key") String project_access_key); + + @GET("create-video/{session_id}") + Call createVideo(@Path("session_id") String sessionId); + + @POST("api/session/create-session-data") + Call sendLocalSessionData(@Body LocalSession localSession); + + @POST("api/device/create") + Call sendDeviceInfo(@Body DeviceModel deviceModel); +} diff --git a/tasklogger/src/main/java/com/phonereplay/tasklogger/network/models/File.java b/tasklogger/src/main/java/com/phonereplay/tasklogger/network/models/File.java new file mode 100644 index 0000000..627a1f2 --- /dev/null +++ b/tasklogger/src/main/java/com/phonereplay/tasklogger/network/models/File.java @@ -0,0 +1,17 @@ +package com.phonereplay.tasklogger.network.models; + +import com.google.gson.annotations.SerializedName; + +public class File { + + @SerializedName("content") + String content; + + @SerializedName("session_id") + String sessionId; + + public File(String content, String sessionId) { + this.content = content; + this.sessionId = sessionId; + } +} diff --git a/tasklogger/src/main/java/com/phonereplay/tasklogger/network/models/Project.java b/tasklogger/src/main/java/com/phonereplay/tasklogger/network/models/Project.java new file mode 100644 index 0000000..8dbe65c --- /dev/null +++ b/tasklogger/src/main/java/com/phonereplay/tasklogger/network/models/Project.java @@ -0,0 +1,23 @@ +package com.phonereplay.tasklogger.network.models; + +import com.google.gson.annotations.SerializedName; + +public class Project { + + @SerializedName("id") + String projectId; + @SerializedName("name") + String name; + @SerializedName("expiration") + String expiration; + @SerializedName("user_id") + String userId; + @SerializedName("project_access_key") + String projectAccessId; + + public String getProjectId() { + return projectId; + } + + +} diff --git a/tasklogger/src/main/java/com/phonereplay/tasklogger/network/models/Session.java b/tasklogger/src/main/java/com/phonereplay/tasklogger/network/models/Session.java new file mode 100644 index 0000000..8ece80c --- /dev/null +++ b/tasklogger/src/main/java/com/phonereplay/tasklogger/network/models/Session.java @@ -0,0 +1,16 @@ +package com.phonereplay.tasklogger.network.models; + +import com.google.gson.annotations.SerializedName; + +public class Session { + + @SerializedName("id") + String sessionId; + @SerializedName("project_id") + String projectId; + + public String getSessionId() { + return sessionId; + } + +} diff --git a/tasklogger/src/main/java/com/phonereplay/tasklogger/network/models/reponses/CreateSessionResponse.java b/tasklogger/src/main/java/com/phonereplay/tasklogger/network/models/reponses/CreateSessionResponse.java new file mode 100644 index 0000000..ac44804 --- /dev/null +++ b/tasklogger/src/main/java/com/phonereplay/tasklogger/network/models/reponses/CreateSessionResponse.java @@ -0,0 +1,20 @@ +package com.phonereplay.tasklogger.network.models.reponses; + +import com.google.gson.annotations.SerializedName; +import com.phonereplay.tasklogger.network.models.Session; + +public class CreateSessionResponse { + + @SerializedName("session") + Session session; + @SerializedName("created") + private boolean created; + + public Session getSession() { + return session; + } + + public boolean isCreated() { + return created; + } +} diff --git a/tasklogger/src/main/java/com/phonereplay/tasklogger/network/models/reponses/VerifyProjectAuthResponse.java b/tasklogger/src/main/java/com/phonereplay/tasklogger/network/models/reponses/VerifyProjectAuthResponse.java new file mode 100644 index 0000000..2844bd2 --- /dev/null +++ b/tasklogger/src/main/java/com/phonereplay/tasklogger/network/models/reponses/VerifyProjectAuthResponse.java @@ -0,0 +1,22 @@ +package com.phonereplay.tasklogger.network.models.reponses; + +import com.google.gson.annotations.SerializedName; +import com.phonereplay.tasklogger.network.models.Project; + +public class VerifyProjectAuthResponse { + + @SerializedName("project") + Project project; + + @SerializedName("auth") + private boolean auth; + + + public Project getProject() { + return project; + } + + public boolean isAuth() { + return auth; + } +} diff --git a/tasklogger/src/main/java/com/phonereplay/tasklogger/reflect/Reflect.java b/tasklogger/src/main/java/com/phonereplay/tasklogger/reflect/Reflect.java new file mode 100755 index 0000000..6f1ac98 --- /dev/null +++ b/tasklogger/src/main/java/com/phonereplay/tasklogger/reflect/Reflect.java @@ -0,0 +1,639 @@ +package com.phonereplay.tasklogger.reflect; + +import android.util.Log; + +import java.lang.reflect.AccessibleObject; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Member; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.lang.reflect.Proxy; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * Overview
+ * A set of novel reflection tools, + * Able to easily implement reflection and make the code highly readable. + * + * @author lody + */ +public class Reflect { + + public static final String TAG = "$$ Reflect"; + + private final Object object; + + private final boolean isClass; + + private Reflect(Class type) { + this.object = type; + this.isClass = true; + } + + private Reflect(Object object) { + this.object = object; + this.isClass = false; + } + + // --------------------------------------------------------------------- + // Member + // --------------------------------------------------------------------- + + /** + * Encapsulate Class.forName(name) + *

+ * Can be called like this: on(Class.forName(name)) + * + * @param name complete class name + * @return tool class itself + * @see #on(Class) + */ + public static Reflect on(String name) throws ReflectException { + return on(forName(name)); + } + + /** + * Encapsulate Class.forName(name) + *

+ * Can be called like this: on(Xxx.class) + * + * @param clazz class + * @return tool class itself + * @see #on(Class) + */ + public static Reflect on(Class clazz) { + return new Reflect(clazz); + } + + // --------------------------------------------------------------------- + // 构造器 + // --------------------------------------------------------------------- + + /** + * Wrap an object + *

+ * You can use this method when you need to access the fields and methods of the instance + * {@link Object} + * + * @param object The object that needs to be packaged + * @return tool class itself + */ + public static Reflect on(Object object) { + return new Reflect(object); + } + + /** + * Convert objects with restricted access to unrestricted. + * Generally, + * Private fields and methods of a class cannot be obtained and called. + * The reason is that Java will check whether it has access permissions before calling. + * When this method is called, + * Access permission checking mechanism will be turned off. + * + * @param accessible object with restricted access + * @return object without access restrictions + */ + public static T accessible(T accessible) { + if (accessible == null) { + return null; + } + + if (accessible instanceof Member) { + Member member = (Member) accessible; + + if (Modifier.isPublic(member.getModifiers()) && + Modifier.isPublic(member.getDeclaringClass().getModifiers())) { + + return accessible; + } + } + + //The default is false, which means access permissions are checked during reflection. + // When set to true, access permissions are not checked and private fields and methods can be accessed. + if (!accessible.isAccessible()) { + accessible.setAccessible(true); + } + + return accessible; + } + + // --------------------------------------------------------------------- + // Excellent API that eliminates the clutter :) + // --------------------------------------------------------------------- + + private static String property(String string) { + int length = string.length(); + + if (length == 0) { + return ""; + } else if (length == 1) { + return string.toLowerCase(); + } else { + return string.substring(0, 1).toLowerCase() + string.substring(1); + } + } + + private static Reflect on(Constructor constructor, Object... args) throws ReflectException { + try { + return on(accessible(constructor).newInstance(args)); + } catch (Exception e) { + throw new ReflectException(e); + } + } + + private static Reflect on(Method method, Object object, Object... args) throws ReflectException { + try { + accessible(method); + + if (method.getReturnType() == void.class) { + method.invoke(object, args); + return on(object); + } else { + return on(method.invoke(object, args)); + } + } catch (Exception e) { + throw new ReflectException(e); + } + } + + /** + * Inner class, taking an object out of its packaging + */ + private static Object unwrap(Object object) { + if (object instanceof Reflect) { + return ((Reflect) object).get(); + } + + return object; + } + + /** + * Inner class, given a series of parameters, returns their types + * + * @see Object#getClass() + */ + private static Class[] types(Object... values) { + if (values == null) { + //空 + return new Class[0]; + } + + Class[] result = new Class[values.length]; + + for (int i = 0; i < values.length; i++) { + Object value = values[i]; + result[i] = value == null ? NULL.class : value.getClass(); + } + + return result; + } + + /** + * 加载一个类 + * + * @see Class#forName(String) + */ + private static Class forName(String name) throws ReflectException { + try { + return Class.forName(name); + } catch (Exception e) { + throw new ReflectException(e); + } + } + + /** + * Get the type of wrapped object, + * If it is a basic type, such as int, float, boolean, etc. + * Then it will be converted to the corresponding object type. + */ + public static Class wrapper(Class type) { + if (type == null) { + return null; + } else if (type.isPrimitive()) { + if (boolean.class == type) { + return Boolean.class; + } else if (int.class == type) { + return Integer.class; + } else if (long.class == type) { + return Long.class; + } else if (short.class == type) { + return Short.class; + } else if (byte.class == type) { + return Byte.class; + } else if (double.class == type) { + return Double.class; + } else if (float.class == type) { + return Float.class; + } else if (char.class == type) { + return Character.class; + } else if (void.class == type) { + return Void.class; + } + } + + return type; + } + + public static void showMethod(Method method) { + showMethod(method.getName(), method.getParameterTypes()); + } + + public static void showMethod(String name, Class[] types) { + StringBuilder msg = new StringBuilder(name + "( "); + for (Class t : types) { + msg.append(t.getName() + ", "); + } + if (types.length != 0) + msg.delete(msg.length() - 2, msg.length()); + msg.append(")"); + Log.d(TAG, msg.toString()); + } + + /** + * 得到当前包装的对象 + */ + public T get() { + //The benefits of generics are immediately apparent + return (T) object; + } + + /** + * Modify the value of a field + *

+ * Equivalent to {@link Field#set(Object, Object)}. If the wrapped object is a + * {@link Class}, then the modification will be a static field, + * If the wrapped object is a {@link Object}, then an instance field is modified. + * + * @param name field name + * @param value field value + * @return tool class after completion + */ + public Reflect set(String name, Object value) throws ReflectException { + try { + Field field = field0(name); + field.set(object, unwrap(value)); + return this; + } catch (Exception e) { + throw new ReflectException(e); + } + } + + public Reflect delete(String name) throws ReflectException { + try { + Field field = field0(name); + field.set(object, null); + return this; + } catch (Exception e) { + throw new ReflectException(e); + } + } + + /** + * 得到字段对值 + * + * @param name 字段名 + * @return The field value + * @see #field(String) + */ + public T get(String name) throws ReflectException { + return field(name).get(); + } + + /** + * Get fields + * + * @param name field name + * @return field + */ + public Reflect field(String name) throws ReflectException { + try { + Field field = field0(name); + return on(field.get(object)); + } catch (Exception e) { + throw new ReflectException(e); + } + } + + private Field field0(String name) throws ReflectException { + Class type = type(); + + // Try to treat it as a public field + try { + return type.getField(name); + } + + // Try to handle it privately + catch (NoSuchFieldException e) { + do { + try { + return accessible(type.getDeclaredField(name)); + } catch (NoSuchFieldException ignore) { + } + + type = type.getSuperclass(); + } + while (type != null); + + throw new ReflectException(e); + } + } + + /** + * Map all objects of an object into a Map, and the key is the field name. + * + * @return map containing all fields + */ + public Map fields() { + Map result = new LinkedHashMap(); + Class type = type(); + + do { + for (Field field : type.getDeclaredFields()) { + if (!isClass ^ Modifier.isStatic(field.getModifiers())) { + String name = field.getName(); + + if (!result.containsKey(name)) + result.put(name, field(name)); + } + } + + type = type.getSuperclass(); + } + while (type != null); + + return result; + } + + // --------------------------------------------------------------------- + // ObjectAPI + // --------------------------------------------------------------------- + + /** + * Given the method name, call the parameterless method + *

+ * Equivalent to call(name, new Object[0]) + * + * @param name method name + * @return tool class itself + * @see #call(String, Object...) + */ + public Reflect call(String name) throws ReflectException { + return call(name, new Object[0]); + } + + /** + * Calls a method given a method name and parameters. + *

+ * Encapsulated from {@link Method#invoke(Object, Object...)}, which can accept basic types + * + * @param name method name + * @param args method parameters + * @return tool class itself + */ + public Reflect call(String name, Object... args) throws ReflectException { + Class[] types = types(args); + + // try to call method + try { + Method method = exactMethod(name, types); + return on(method, object, args); + } + + //If there is no method matching the parameters, + // Match the method closest to the method name. + catch (NoSuchMethodException e) { + try { + Log.d(TAG, "no exact method found, try to find the similar one!"); + Method method = similarMethod(name, types); + showMethod(method); + return on(method, object, args); + } catch (NoSuchMethodException e1) { + Log.e(TAG, "no similar found!"); + throw new ReflectException(e1); + } + } + } + + /** + * Get the method based on the method name and method parameters + */ + private Method exactMethod(String name, Class[] types) throws NoSuchMethodException { + Class type = type(); + + // 先尝试直接调用 + try { + showMethod(name, types); + return type.getMethod(name, types); + } + + //Maybe this is a private method + catch (NoSuchMethodException e) { + do { + try { + return type.getDeclaredMethod(name, types); + } catch (NoSuchMethodException ignore) { + } + + type = type.getSuperclass(); + } + while (type != null); + + throw new NoSuchMethodException(); + } + } + + /** + * Given a method name and parameters, match the closest method + */ + private Method similarMethod(String name, Class[] types) throws NoSuchMethodException { + Class type = type(); + + //对于公有方法: + for (Method method : type.getMethods()) { + if (isSimilarSignature(method, name, types)) { + return method; + } + } + + //对于私有方法: + do { + for (Method method : type.getDeclaredMethods()) { + if (isSimilarSignature(method, name, types)) { + return method; + } + } + + type = type.getSuperclass(); + } + while (type != null); + + throw new NoSuchMethodException("No similar method " + name + " with params " + Arrays.toString(types) + " could be found on type " + type() + "."); + } + + private List convertObjectToList(Object obj) { + List list = new ArrayList<>(); + if (obj.getClass().isArray()) { + list = Arrays.asList((Object[]) obj); + } else if (obj instanceof Collection) { + list = new ArrayList<>((Collection) obj); + } + return list; + } + + // --------------------------------------------------------------------- + // internal tool methods + // --------------------------------------------------------------------- + + /** + * Confirm again whether the method signature matches the actual one, + * Convert basic types into corresponding object types, + * If int is converted to Int + */ + private boolean isSimilarSignature(Method possiblyMatchingMethod, String desiredMethodName, Class[] desiredParamTypes) { + return possiblyMatchingMethod.getName().equals(desiredMethodName) && match(possiblyMatchingMethod.getParameterTypes(), desiredParamTypes); + } + + /** + * Call a parameterless constructor + *

+ * Equivalent to create(new Object[0]) + * + * @return tool class itself + * @see #create(Object...) + */ + public Reflect create() throws ReflectException { + return create(new Object[0]); + } + + /** + * Call a parameterized constructor + * + * @param args constructor parameters + * @return tool class itself + */ + public Reflect create(Object... args) throws ReflectException { + Class[] types = types(args); + + + try { + Constructor constructor = type().getDeclaredConstructor(types); + return on(constructor, args); + } + + //In this case, the constructor is often private, mostly used in factory methods, and the constructor is deliberately hidden. + catch (NoSuchMethodException e) { + //private阻止不了反射的脚步:) + for (Constructor constructor : type().getDeclaredConstructors()) { + if (match(constructor.getParameterTypes(), types)) { + return on(constructor, args); + } + } + + throw new ReflectException(e); + } + } + + /** + * Create a proxy for the wrapped object. + * + * @param proxyType proxy type + * @return The delegate of the wrapping object. + */ + @SuppressWarnings("unchecked") + public

P as(Class

proxyType) { + final boolean isMap = (object instanceof Map); + final InvocationHandler handler = new InvocationHandler() { + @SuppressWarnings("null") + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + String name = method.getName(); + + try { + return on(object).call(name, args).get(); + } catch (ReflectException e) { + if (isMap) { + Map map = (Map) object; + int length = (args == null ? 0 : args.length); + + if (length == 0 && name.startsWith("get")) { + return map.get(property(name.substring(3))); + } else if (length == 0 && name.startsWith("is")) { + return map.get(property(name.substring(2))); + } else if (length == 1 && name.startsWith("set")) { + map.put(property(name.substring(3)), args[0]); + return null; + } + } + + throw e; + } + } + }; + + return (P) Proxy.newProxyInstance(proxyType.getClassLoader(), new Class[]{proxyType}, handler); + } + + private boolean match(Class[] declaredTypes, Class[] actualTypes) { + if (declaredTypes.length == actualTypes.length) { + for (int i = 0; i < actualTypes.length; i++) { + if (actualTypes[i] == NULL.class) + continue; + + if (wrapper(declaredTypes[i]).isAssignableFrom(wrapper(actualTypes[i]))) + continue; + + return false; + } + + return true; + } else { + return false; + } + } + + @Override + public int hashCode() { + return object.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof Reflect) { + return object.equals(((Reflect) obj).get()); + } + + return false; + } + + @Override + public String toString() { + return object.toString(); + } + + /** + * Get the type of wrapped object + * + * @see Object#getClass() + */ + public Class type() { + if (isClass) { + return (Class) object; + } else { + return object.getClass(); + } + } + + /** + * defines a null type + */ + private static class NULL { + } + +} diff --git a/tasklogger/src/main/java/com/phonereplay/tasklogger/reflect/ReflectException.java b/tasklogger/src/main/java/com/phonereplay/tasklogger/reflect/ReflectException.java new file mode 100755 index 0000000..95e4ace --- /dev/null +++ b/tasklogger/src/main/java/com/phonereplay/tasklogger/reflect/ReflectException.java @@ -0,0 +1,25 @@ +package com.phonereplay.tasklogger.reflect; + +/** + * Exception thrown when reflection error occurs + */ +public class ReflectException extends RuntimeException { + + private static final long serialVersionUID = -2243843843843438438L; + + public ReflectException(String message) { + super(message); + } + + public ReflectException(String message, Throwable cause) { + super(message, cause); + } + + public ReflectException() { + super(); + } + + public ReflectException(Throwable cause) { + super(cause); + } +} diff --git a/tasklogger/src/main/java/com/phonereplay/tasklogger/service/PhoneReplayService.java b/tasklogger/src/main/java/com/phonereplay/tasklogger/service/PhoneReplayService.java new file mode 100644 index 0000000..94d18d9 --- /dev/null +++ b/tasklogger/src/main/java/com/phonereplay/tasklogger/service/PhoneReplayService.java @@ -0,0 +1,253 @@ +package com.phonereplay.tasklogger.service; + +import android.content.Context; +import android.graphics.Bitmap; +import android.util.Log; + +import androidx.annotation.NonNull; + +import com.phonereplay.tasklogger.DeviceModel; +import com.phonereplay.tasklogger.LocalSession; +import com.phonereplay.tasklogger.TimeLine; +import com.phonereplay.tasklogger.network.ApiClient; +import com.phonereplay.tasklogger.network.GrpcClient; +import com.phonereplay.tasklogger.network.models.reponses.CreateSessionResponse; +import com.phonereplay.tasklogger.network.models.reponses.VerifyProjectAuthResponse; +import com.phonereplay.tasklogger.utils.NetworkUtil; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.Arrays; +import java.util.Set; +import java.util.zip.Deflater; + +import retrofit2.Call; +import retrofit2.Callback; +import retrofit2.Response; + +public class PhoneReplayService { + private static final int COMPRESSION_QUALITY = 10; + private final ApiClient apiClient; + private final GrpcClient grpcClient; + private final Context context; + private byte[] fullBytesVideo; + private byte[] previousImageCompressed; + private VerifyProjectAuthResponse verifyProjectAuthResponse; + private CreateSessionResponse session; + private String accessKey; + + public PhoneReplayService(Context context) { + this.apiClient = new ApiClient(); + this.grpcClient = new GrpcClient(); + this.context = context; + } + + private static byte[] joinByteArrays(byte[] array1, byte[] array2) { + int length1 = array1.length; + int length2 = array2.length; + byte[] result = new byte[length1 + length2]; + System.arraycopy(array1, 0, result, 0, length1); + System.arraycopy(array2, 0, result, length1, length2); + return result; + } + + public static byte[] compress(byte[] data) throws IOException { + if (data == null) { + return null; + } + Deflater deflater = new Deflater(Deflater.DEFAULT_COMPRESSION); + try { + deflater.setInput(data); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(data.length); + deflater.finish(); + byte[] buffer = new byte[1024]; + while (!deflater.finished()) { + int count = deflater.deflate(buffer); + outputStream.write(buffer, 0, count); + } + outputStream.close(); + return outputStream.toByteArray(); + } finally { + deflater.end(); + } + } + + /* + private static String encodeToBase64(byte[] binaryData) { + byte[] base64Encoded = android.util.Base64.encode(binaryData, Base64.DEFAULT); + return new String(base64Encoded); + } + */ + + private static byte[] writeImageCompressedFromBitmap(Bitmap bitmap) throws IOException { + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + bitmap.compress(Bitmap.CompressFormat.JPEG, COMPRESSION_QUALITY, byteArrayOutputStream); + return compress(byteArrayOutputStream.toByteArray()); + } + + private static byte[] writeImageFromBitmap(Bitmap bitmap) throws IOException { + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + bitmap.compress(Bitmap.CompressFormat.JPEG, COMPRESSION_QUALITY, byteArrayOutputStream); + return byteArrayOutputStream.toByteArray(); + } + + public static boolean compareByteArrays(byte[] array1, byte[] array2) { + return Arrays.equals(array1, array2); + } + + private static byte[] combineIdentifierAndData(byte[] data) { + byte[] identifierBytes = "------".getBytes(); + byte[] combinedData = new byte[identifierBytes.length + data.length]; + + System.arraycopy(identifierBytes, 0, combinedData, 0, identifierBytes.length); + System.arraycopy(data, 0, combinedData, identifierBytes.length, data.length); + + return combinedData; + } + + public CreateSessionResponse getSession() { + return session; + } + + public VerifyProjectAuthResponse getVerifyProjectAuthResponse() { + return verifyProjectAuthResponse; + } + + public void queueBytesBitmap(Bitmap bitmap) throws IOException { + byte[] imageCompressed = writeImageCompressedFromBitmap(bitmap); + byte[] combineIdentifierAndData; + + if (fullBytesVideo != null) { + if (compareByteArrays(previousImageCompressed, imageCompressed)) { + combineIdentifierAndData = combineIdentifierAndData("D".getBytes()); + } else { + combineIdentifierAndData = combineIdentifierAndData(imageCompressed); + previousImageCompressed = imageCompressed; + } + byte[] joinByteArrays; + joinByteArrays = joinByteArrays(fullBytesVideo, combineIdentifierAndData); + fullBytesVideo = joinByteArrays; + } else { + previousImageCompressed = imageCompressed; + combineIdentifierAndData = imageCompressed; + fullBytesVideo = combineIdentifierAndData; + } + bitmap.recycle(); + } + + public void queueBytesBitmapV2(Bitmap bitmap) throws IOException { + byte[] image = writeImageFromBitmap(bitmap); + byte[] combineIdentifierAndData; + + if (fullBytesVideo != null) { + if (compareByteArrays(previousImageCompressed, image)) { + combineIdentifierAndData = combineIdentifierAndData("D".getBytes()); + } else { + combineIdentifierAndData = combineIdentifierAndData(image); + previousImageCompressed = image; + } + byte[] joinByteArrays; + joinByteArrays = joinByteArrays(fullBytesVideo, combineIdentifierAndData); + fullBytesVideo = joinByteArrays; + } else { + previousImageCompressed = image; + combineIdentifierAndData = image; + fullBytesVideo = combineIdentifierAndData; + } + bitmap.recycle(); + } + + public CreateSessionResponse createSession(String projectId) { + if (projectId == null) { + return null; + } + + Call call = apiClient.createSession(projectId); + try { + Response createSessionResponseResponse = call.execute(); + if (createSessionResponseResponse.isSuccessful()) { + session = createSessionResponseResponse.body(); + return session; + } else { + return null; + } + } catch (IOException e) { + e.printStackTrace(); + return null; + } + } + + public void sendLocalSessionData(LocalSession localSession) { + Call call = apiClient.sendLocalSessionData(localSession); + call.enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + if (response.isSuccessful()) { + Log.d("Upload", "Dados enviados com sucesso."); + } else { + Log.d("Upload", "Falha ao enviar dados. Código de resposta: " + response.code()); + } + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + Log.d("Upload", "Erro na chamada de rede", t); + } + }); + } + + public void sendDeviceInfo(DeviceModel deviceModel) { + Call call = apiClient.sendDeviceInfo(deviceModel); + call.enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + if (response.isSuccessful()) { + Log.d("Upload", "Dados enviados com sucesso."); + } else { + Log.d("Upload", "Falha ao enviar dados. Código de resposta: " + response.code()); + } + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + Log.d("Upload", "Erro na chamada de rede", t); + } + }); + } + + + public void verifyProjectAuth(String project_access_key) { + this.accessKey = project_access_key; + Call call = apiClient.verifyProjectAuth(project_access_key); + + try { + Response createSessionResponseResponse = call.execute(); + verifyProjectAuthResponse = createSessionResponseResponse.body(); + } catch (IOException e) { + System.out.println(); + // handle error + } + } + + public void verifyProjectAuth() { + if (accessKey == null) { + return; + } + + Call call = apiClient.verifyProjectAuth(accessKey); + try { + Response createSessionResponseResponse = call.execute(); + verifyProjectAuthResponse = createSessionResponseResponse.body(); + } catch (IOException e) { + System.out.println(); + } + } + + public void createVideo(String sessionId, Set timeLines) throws IOException { + grpcClient.sendBinaryData(compress(fullBytesVideo), sessionId, timeLines); + //fullBytesVideo = null; + if (NetworkUtil.isWiFiConnected(context)) { + } else { + } + } +} diff --git a/tasklogger/src/main/java/com/phonereplay/tasklogger/utils/BitmapUtils.java b/tasklogger/src/main/java/com/phonereplay/tasklogger/utils/BitmapUtils.java new file mode 100644 index 0000000..c0ce4a7 --- /dev/null +++ b/tasklogger/src/main/java/com/phonereplay/tasklogger/utils/BitmapUtils.java @@ -0,0 +1,46 @@ +package com.phonereplay.tasklogger.utils; + +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Matrix; +import android.view.View; + +public class BitmapUtils { + + private static Bitmap captureAndResizeView(Bitmap originalBitmap, int originalWidth, int originalHeight, int newHeight) { + //if (this.orientation) { + // return resizeBitmap(rotateBitmap(originalBitmap, 90), mainWidth, mainHeight); + //} + float aspectRatio = (float) originalWidth / originalHeight; + int newWidth = Math.round(newHeight * aspectRatio); + + return Bitmap.createScaledBitmap(originalBitmap, newWidth, newHeight, true); + } + + public static Bitmap convertViewToDrawable(View view) { + Bitmap bitmap = Bitmap.createBitmap(view.getMeasuredWidth(), view.getMeasuredHeight(), Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(bitmap); + canvas.translate(-view.getScrollX(), -view.getScrollY()); + view.draw(canvas); + return captureAndResizeView(bitmap, view.getMeasuredWidth(), view.getMeasuredHeight(), view.getMeasuredHeight()); + } + + public Bitmap rotateBitmap(Bitmap original, float degrees) { + Matrix matrix = new Matrix(); + matrix.postRotate(degrees); + return Bitmap.createBitmap(original, 0, 0, original.getWidth(), original.getHeight(), matrix, true); + } + + public Bitmap resizeBitmap(Bitmap original, int newWidth, int newHeight) { + int width = original.getWidth(); + int height = original.getHeight(); + + float scaleWidth = ((float) newWidth) / width; + float scaleHeight = ((float) newHeight) / height; + + Matrix matrix = new Matrix(); + matrix.postScale(scaleWidth, scaleHeight); + + return Bitmap.createBitmap(original, 0, 0, width, height, matrix, false); + } +} diff --git a/tasklogger/src/main/java/com/phonereplay/tasklogger/utils/NetworkUtil.java b/tasklogger/src/main/java/com/phonereplay/tasklogger/utils/NetworkUtil.java new file mode 100644 index 0000000..0199a52 --- /dev/null +++ b/tasklogger/src/main/java/com/phonereplay/tasklogger/utils/NetworkUtil.java @@ -0,0 +1,56 @@ +package com.phonereplay.tasklogger.utils; + +import android.content.Context; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; + +import java.net.InetAddress; +import java.net.NetworkInterface; +import java.net.SocketException; +import java.util.Enumeration; + +public class NetworkUtil { + + public static boolean isWiFiConnected(Context context) { + ConnectivityManager connectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); + if (connectivityManager != null) { + NetworkInfo networkInfo = connectivityManager.getActiveNetworkInfo(); + return networkInfo != null && networkInfo.getType() == ConnectivityManager.TYPE_WIFI && networkInfo.isConnected(); + } + return false; + } + + public static String getIPAddress(boolean useIPv4) { + try { + Enumeration interfaces = NetworkInterface.getNetworkInterfaces(); + while (interfaces.hasMoreElements()) { + NetworkInterface intf = interfaces.nextElement(); + Enumeration addresses = intf.getInetAddresses(); + while (addresses.hasMoreElements()) { + InetAddress addr = addresses.nextElement(); + // Filtra endereços loopback e verifica se o endereço é IPv4 se necessário + if (!addr.isLoopbackAddress()) { + String sAddr = addr.getHostAddress(); + // O método isIPv4Address verifica se o endereço é IPv4 + assert sAddr != null; + boolean isIPv4 = sAddr.indexOf(':') < 0; + + if (useIPv4) { + if (isIPv4) + return sAddr; + } else { + if (!isIPv4) { + // Converte o endereço IPv6 para a forma compressa + int delim = sAddr.indexOf('%'); // Trata endereço com zona de escopo + return delim < 0 ? sAddr.toUpperCase() : sAddr.substring(0, delim).toUpperCase(); + } + } + } + } + } + } catch (SocketException e) { + e.printStackTrace(); + } + return ""; + } +} diff --git a/tasklogger/src/test/java/com/phonereplay/tasklogger/ExampleUnitTest.java b/tasklogger/src/test/java/com/phonereplay/tasklogger/ExampleUnitTest.java new file mode 100644 index 0000000..4ee66ef --- /dev/null +++ b/tasklogger/src/test/java/com/phonereplay/tasklogger/ExampleUnitTest.java @@ -0,0 +1,17 @@ +package com.phonereplay.tasklogger; + +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