Initial import master
authorAntonio Ospite <ao2@ao2.it>
Mon, 4 Jan 2016 11:51:55 +0000 (12:51 +0100)
committerAntonio Ospite <ao2@ao2.it>
Mon, 4 Jan 2016 12:43:40 +0000 (13:43 +0100)
40 files changed:
.gitignore [new file with mode: 0644]
TODO [new file with mode: 0644]
artwork/ic_launcher.svg [new file with mode: 0644]
build.gradle [new file with mode: 0644]
gradle/wrapper/gradle-wrapper.jar [new file with mode: 0644]
gradle/wrapper/gradle-wrapper.properties [new file with mode: 0644]
gradlew [new file with mode: 0755]
gradlew.bat [new file with mode: 0644]
src/androidTest/java/it/ao2/savemysugar/MainActivityTest.java [new file with mode: 0644]
src/main/AndroidManifest.xml [new file with mode: 0644]
src/main/java/it/ao2/savemysugar/MainActivity.java [new file with mode: 0644]
src/main/java/it/ao2/savemysugar/SaveMySugarApplication.java [new file with mode: 0644]
src/main/java/it/ao2/savemysugar/SettingsActivity.java [new file with mode: 0644]
src/main/java/it/ao2/savemysugar/fragments/ReceiveFragment.java [new file with mode: 0644]
src/main/java/it/ao2/savemysugar/fragments/SendFragment.java [new file with mode: 0644]
src/main/java/it/ao2/savemysugar/morse/MorseDistanceModulator.java [new file with mode: 0644]
src/main/java/it/ao2/savemysugar/morse/MorseTranslator.java [new file with mode: 0644]
src/main/java/it/ao2/savemysugar/util/TimeLog.java [new file with mode: 0644]
src/main/res/drawable-hdpi/ic_launcher.png [new file with mode: 0644]
src/main/res/drawable-ldpi/ic_launcher.png [new file with mode: 0644]
src/main/res/drawable-mdpi/ic_launcher.png [new file with mode: 0644]
src/main/res/drawable-xhdpi/ic_launcher.png [new file with mode: 0644]
src/main/res/drawable-xxhdpi/ic_launcher.png [new file with mode: 0644]
src/main/res/drawable-xxxhdpi/ic_launcher.png [new file with mode: 0644]
src/main/res/layout/about.xml [new file with mode: 0644]
src/main/res/layout/activity_main.xml [new file with mode: 0644]
src/main/res/layout/activity_settings.xml [new file with mode: 0644]
src/main/res/layout/fragment_receive.xml [new file with mode: 0644]
src/main/res/layout/fragment_send.xml [new file with mode: 0644]
src/main/res/layout/toolbar.xml [new file with mode: 0644]
src/main/res/menu/main.xml [new file with mode: 0644]
src/main/res/values-v14/styles.xml [new file with mode: 0644]
src/main/res/values-v21/styles.xml [new file with mode: 0644]
src/main/res/values/colors.xml [new file with mode: 0644]
src/main/res/values/compat.xml [new file with mode: 0644]
src/main/res/values/dimens.xml [new file with mode: 0644]
src/main/res/values/donottranslate.xml [new file with mode: 0644]
src/main/res/values/strings.xml [new file with mode: 0644]
src/main/res/values/styles.xml [new file with mode: 0644]
src/main/res/xml/preferences.xml [new file with mode: 0644]

diff --git a/.gitignore b/.gitignore
new file mode 100644 (file)
index 0000000..bee0491
--- /dev/null
@@ -0,0 +1,3 @@
+.gradle/
+build
+local.properties
diff --git a/TODO b/TODO
new file mode 100644 (file)
index 0000000..165068e
--- /dev/null
+++ b/TODO
@@ -0,0 +1,13 @@
+- Factor out the parameters update code in ReceiveFragment and SendFragment
+- When using PreferenceFragmentCompat, inputType is ignored for
+  EditTextPreferences, find a solution. the bug report is here
+  https://code.google.com/p/android/issues/detail?id=185164
+- See if polluting the incoming calls log can be avoided
+- Once a message is received add it to the normal SMS storage:
+  https://groups.google.com/forum/#!topic/android-developers/8XnKPeO7AWE
+- Use a Service for receiving the message (for sending them too?)
+- Use Intent.ACTION_PICK to pickup the destination number instead of typing it
+  by hand.
+- Simplify the parameters, see if they are all really necessary
+- Possibly add parameters validation on input
+- Maybe make the Morse classes independent packages
diff --git a/artwork/ic_launcher.svg b/artwork/ic_launcher.svg
new file mode 100644 (file)
index 0000000..f778d02
--- /dev/null
@@ -0,0 +1,190 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:xlink="http://www.w3.org/1999/xlink"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="48"
+   height="48"
+   id="svg2"
+   version="1.1"
+   inkscape:version="0.91 r13725"
+   sodipodi:docname="ic_launcher.svg"
+   inkscape:export-filename="ic_launcher.png"
+   inkscape:export-xdpi="90"
+   inkscape:export-ydpi="90">
+  <defs
+     id="defs4">
+    <linearGradient
+       id="linearGradient4114">
+      <stop
+         style="stop-color:#ffffff;stop-opacity:1;"
+         offset="0"
+         id="stop4116" />
+      <stop
+         id="stop4118"
+         offset="0.36040047"
+         style="stop-color:#e2f2b0;stop-opacity:1" />
+      <stop
+         style="stop-color:#c6e26e;stop-opacity:1"
+         offset="0.38111261"
+         id="stop4120" />
+      <stop
+         id="stop4122"
+         offset="0.53974605"
+         style="stop-color:#b6d850;stop-opacity:1" />
+      <stop
+         style="stop-color:#accf45;stop-opacity:1"
+         offset="0.55895942"
+         id="stop4124" />
+      <stop
+         style="stop-color:#a4c639;stop-opacity:1"
+         offset="1"
+         id="stop4126" />
+    </linearGradient>
+    <radialGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient4114"
+       id="radialGradient3038"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="matrix(-0.27111876,0.50302922,-0.53215558,-0.2868171,694.31841,1062.2212)"
+       spreadMethod="pad"
+       cx="473.27908"
+       cy="1001.8383"
+       fx="473.27908"
+       fy="1001.8383"
+       r="92.698822" />
+    <filter
+       inkscape:collect="always"
+       id="filter4151">
+      <feGaussianBlur
+         inkscape:collect="always"
+         stdDeviation="1"
+         id="feGaussianBlur4153" />
+    </filter>
+  </defs>
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageopacity="0.0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="5.3854165"
+     inkscape:cx="23.414212"
+     inkscape:cy="17.684468"
+     inkscape:document-units="px"
+     inkscape:current-layer="layer1"
+     showgrid="false"
+     inkscape:window-width="1280"
+     inkscape:window-height="910"
+     inkscape:window-x="0"
+     inkscape:window-y="28"
+     inkscape:window-maximized="1"
+     showguides="true"
+     inkscape:guide-bbox="true">
+    <sodipodi:guide
+       orientation="1,0"
+       position="4,0"
+       id="guide3004" />
+    <sodipodi:guide
+       orientation="0,1"
+       position="0,4"
+       id="guide3006" />
+    <sodipodi:guide
+       orientation="0,1"
+       position="0,44"
+       id="guide3008" />
+    <sodipodi:guide
+       orientation="1,0"
+       position="44,0"
+       id="guide3010" />
+  </sodipodi:namedview>
+  <metadata
+     id="metadata7">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title />
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:label="Livello 1"
+     inkscape:groupmode="layer"
+     id="layer1"
+     transform="translate(0,-1004.3622)">
+    <rect
+       style="color:#000000;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.99971437;marker:none;visibility:visible;display:inline;overflow:visible;filter:url(#filter4151);enable-background:accumulate"
+       id="rect2837-1"
+       width="40"
+       height="40"
+       x="4"
+       y="1009.3622"
+       rx="4.323647"
+       ry="4.3236475"
+       inkscape:export-xdpi="300"
+       inkscape:export-ydpi="300" />
+    <g
+       transform="matrix(0,0.05312697,-0.05312697,0,60.172753,1008.6022)"
+       id="layer1-9"
+       inkscape:label="Livello 1"
+       style="stroke:#1976d2">
+      <rect
+         inkscape:export-ydpi="300"
+         inkscape:export-xdpi="300"
+         ry="79.349289"
+         rx="79.349281"
+         y="-739.09515"
+         x="313.93329"
+         height="734.09583"
+         width="734.09583"
+         id="rect2837"
+         style="color:#000000;display:inline;overflow:visible;visibility:visible;fill:#ffffff;fill-opacity:1;stroke:#1976d2;stroke-width:18.81745529;stroke-linecap:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate"
+         transform="matrix(0,1,-1,0,0,0)" />
+      <g
+         id="g3136"
+         style="stroke:#1976d2">
+        <circle
+           inkscape:export-ydpi="300"
+           inkscape:export-xdpi="300"
+           inkscape:export-filename="rect2837.png"
+           style="color:#000000;display:inline;overflow:visible;visibility:visible;fill:#82b1ff;stroke:#1976d2;stroke-width:18.96922302;stroke-linecap:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate"
+           id="path2839"
+           transform="matrix(0.70143061,0.70146822,-0.70143061,0.70146822,168.1798,241.23622)"
+           cx="162.14285"
+           cy="169.09448"
+           r="66.428574" />
+        <circle
+           inkscape:export-ydpi="300"
+           inkscape:export-xdpi="300"
+           inkscape:export-filename="rect2837.png"
+           transform="matrix(0.70143061,0.70146822,-0.70143061,0.70146822,376.51848,449.58607)"
+           style="color:#000000;display:inline;overflow:visible;visibility:visible;fill:#82b1ff;stroke:#1976d2;stroke-width:18.96922302;stroke-linecap:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate"
+           id="path2839-1"
+           cx="162.14285"
+           cy="169.09448"
+           r="66.428574" />
+        <circle
+           inkscape:export-ydpi="300"
+           inkscape:export-xdpi="300"
+           inkscape:export-filename="rect2837.png"
+           transform="matrix(0.70143061,0.70146822,-0.70143061,0.70146822,582.90236,655.98102)"
+           style="color:#000000;display:inline;overflow:visible;visibility:visible;fill:#82b1ff;stroke:#1976d2;stroke-width:18.96922302;stroke-linecap:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate"
+           id="path2839-7"
+           cx="162.14285"
+           cy="169.09448"
+           r="66.428574" />
+      </g>
+    </g>
+  </g>
+</svg>
diff --git a/build.gradle b/build.gradle
new file mode 100644 (file)
index 0000000..dcb3b1a
--- /dev/null
@@ -0,0 +1,52 @@
+buildscript {
+    repositories {
+        mavenCentral()
+    }
+    dependencies {
+        classpath 'com.android.tools.build:gradle:1.3.1'
+        classpath 'org.ajoberstar:grgit:1.4.1'
+    }
+}
+apply plugin: 'com.android.application'
+
+dependencies {
+    compile 'com.android.support:appcompat-v7:23.1.1'
+    compile 'com.android.support:design:23.1.1'
+    compile 'com.android.support:preference-v7:23.1.1'
+    compile 'com.android.support:preference-v14:23.1.1'
+}
+
+ext {
+  git = org.ajoberstar.grgit.Grgit.open(file('.'))
+  gitVersionCode = git.log().size()
+  gitVersionName = "${git.describe()}"
+}
+
+android {
+    compileSdkVersion 'android-23'
+    buildToolsVersion '23.0.1'
+
+    defaultConfig {
+        minSdkVersion 11
+        targetSdkVersion 23
+
+        versionCode gitVersionCode
+        versionName gitVersionName
+        resValue "string", "app_name", "SaveMySugar"
+        setProperty("archivesBaseName", "SaveMySugar-$versionName")
+    }
+
+    buildTypes {
+        release {
+            minifyEnabled false
+            proguardFile getDefaultProguardFile('proguard-android.txt')
+        }
+    }
+}
+
+allprojects {
+    tasks.withType(JavaCompile) {
+        options.compilerArgs << "-Xlint:deprecation"
+        options.compilerArgs << "-Xlint:unchecked"
+    }
+}
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
new file mode 100644 (file)
index 0000000..8c0fb64
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 (file)
index 0000000..179dd79
--- /dev/null
@@ -0,0 +1,6 @@
+#Wed Apr 10 15:27:10 PDT 2013
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.5-all.zip
diff --git a/gradlew b/gradlew
new file mode 100755 (executable)
index 0000000..91a7e26
--- /dev/null
+++ b/gradlew
@@ -0,0 +1,164 @@
+#!/usr/bin/env bash
+
+##############################################################################
+##
+##  Gradle start up script for UN*X
+##
+##############################################################################
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn ( ) {
+    echo "$*"
+}
+
+die ( ) {
+    echo
+    echo "$*"
+    echo
+    exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+case "`uname`" in
+  CYGWIN* )
+    cygwin=true
+    ;;
+  Darwin* )
+    darwin=true
+    ;;
+  MINGW* )
+    msys=true
+    ;;
+esac
+
+# For Cygwin, ensure paths are in UNIX format before anything is touched.
+if $cygwin ; then
+    [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
+fi
+
+# 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\"`/" >&-
+APP_HOME="`pwd -P`"
+cd "$SAVED" >&-
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+    if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+        # IBM's JDK on AIX uses strange locations for the executables
+        JAVACMD="$JAVA_HOME/jre/sh/java"
+    else
+        JAVACMD="$JAVA_HOME/bin/java"
+    fi
+    if [ ! -x "$JAVACMD" ] ; then
+        die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+    fi
+else
+    JAVACMD="java"
+    which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
+    MAX_FD_LIMIT=`ulimit -H -n`
+    if [ $? -eq 0 ] ; then
+        if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+            MAX_FD="$MAX_FD_LIMIT"
+        fi
+        ulimit -n $MAX_FD
+        if [ $? -ne 0 ] ; then
+            warn "Could not set maximum file descriptor limit: $MAX_FD"
+        fi
+    else
+        warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+    fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+    GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+    APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+    CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+
+    # We build the pattern for arguments to be converted via cygpath
+    ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+    SEP=""
+    for dir in $ROOTDIRSRAW ; do
+        ROOTDIRS="$ROOTDIRS$SEP$dir"
+        SEP="|"
+    done
+    OURCYGPATTERN="(^($ROOTDIRS))"
+    # Add a user-defined pattern to the cygpath arguments
+    if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+        OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+    fi
+    # Now convert the arguments - kludge to limit ourselves to /bin/sh
+    i=0
+    for arg in "$@" ; do
+        CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+        CHECK2=`echo "$arg"|egrep -c "^-"`                                 ### Determine if an option
+
+        if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then                    ### Added a condition
+            eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+        else
+            eval `echo args$i`="\"$arg\""
+        fi
+        i=$((i+1))
+    done
+    case $i in
+        (0) set -- ;;
+        (1) set -- "$args0" ;;
+        (2) set -- "$args0" "$args1" ;;
+        (3) set -- "$args0" "$args1" "$args2" ;;
+        (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+        (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+        (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+        (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+        (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+        (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+    esac
+fi
+
+# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
+function splitJvmOpts() {
+    JVM_OPTS=("$@")
+}
+eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
+JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
+
+exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
diff --git a/gradlew.bat b/gradlew.bat
new file mode 100644 (file)
index 0000000..aec9973
--- /dev/null
@@ -0,0 +1,90 @@
+@if "%DEBUG%" == "" @echo off\r
+@rem ##########################################################################\r
+@rem\r
+@rem  Gradle startup script for Windows\r
+@rem\r
+@rem ##########################################################################\r
+\r
+@rem Set local scope for the variables with windows NT shell\r
+if "%OS%"=="Windows_NT" setlocal\r
+\r
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\r
+set DEFAULT_JVM_OPTS=\r
+\r
+set DIRNAME=%~dp0\r
+if "%DIRNAME%" == "" set DIRNAME=.\r
+set APP_BASE_NAME=%~n0\r
+set APP_HOME=%DIRNAME%\r
+\r
+@rem Find java.exe\r
+if defined JAVA_HOME goto findJavaFromJavaHome\r
+\r
+set JAVA_EXE=java.exe\r
+%JAVA_EXE% -version >NUL 2>&1\r
+if "%ERRORLEVEL%" == "0" goto init\r
+\r
+echo.\r
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.\r
+echo.\r
+echo Please set the JAVA_HOME variable in your environment to match the\r
+echo location of your Java installation.\r
+\r
+goto fail\r
+\r
+:findJavaFromJavaHome\r
+set JAVA_HOME=%JAVA_HOME:"=%\r
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe\r
+\r
+if exist "%JAVA_EXE%" goto init\r
+\r
+echo.\r
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%\r
+echo.\r
+echo Please set the JAVA_HOME variable in your environment to match the\r
+echo location of your Java installation.\r
+\r
+goto fail\r
+\r
+:init\r
+@rem Get command-line arguments, handling Windowz variants\r
+\r
+if not "%OS%" == "Windows_NT" goto win9xME_args\r
+if "%@eval[2+2]" == "4" goto 4NT_args\r
+\r
+:win9xME_args\r
+@rem Slurp the command line arguments.\r
+set CMD_LINE_ARGS=\r
+set _SKIP=2\r
+\r
+:win9xME_args_slurp\r
+if "x%~1" == "x" goto execute\r
+\r
+set CMD_LINE_ARGS=%*\r
+goto execute\r
+\r
+:4NT_args\r
+@rem Get arguments from the 4NT Shell from JP Software\r
+set CMD_LINE_ARGS=%$\r
+\r
+:execute\r
+@rem Setup the command line\r
+\r
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar\r
+\r
+@rem Execute Gradle\r
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%\r
+\r
+:end\r
+@rem End local scope for the variables with windows NT shell\r
+if "%ERRORLEVEL%"=="0" goto mainEnd\r
+\r
+:fail\r
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of\r
+rem the _cmd.exe /c_ return code!\r
+if  not "" == "%GRADLE_EXIT_CONSOLE%" exit 1\r
+exit /b 1\r
+\r
+:mainEnd\r
+if "%OS%"=="Windows_NT" endlocal\r
+\r
+:omega\r
diff --git a/src/androidTest/java/it/ao2/savemysugar/MainActivityTest.java b/src/androidTest/java/it/ao2/savemysugar/MainActivityTest.java
new file mode 100644 (file)
index 0000000..b7571a1
--- /dev/null
@@ -0,0 +1,21 @@
+package it.ao2.savemysugar;
+
+import android.test.ActivityInstrumentationTestCase2;
+
+/**
+ * This is a simple framework for a test of an Application.  See
+ * {@link android.test.ApplicationTestCase ApplicationTestCase} for more information on
+ * how to write and extend Application tests.
+ * <p/>
+ * To run this test, you can type:
+ * adb shell am instrument -w \
+ * -e class it.ao2.savemysugar.MainActivityTest \
+ * it.ao2.savemysugar.tests/android.test.InstrumentationTestRunner
+ */
+public class MainActivityTest extends ActivityInstrumentationTestCase2<MainActivity> {
+
+    public MainActivityTest() {
+        super("it.ao2.savemysugar", MainActivity.class);
+    }
+
+}
diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml
new file mode 100644 (file)
index 0000000..eed1ffd
--- /dev/null
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<manifest
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    package="it.ao2.savemysugar">
+    <uses-permission android:name="android.permission.READ_PHONE_STATE" />
+    <uses-permission android:name="android.permission.CALL_PHONE" />
+    <uses-sdk xmlns:tools="http://schemas.android.com/tools"
+        tools:overrideLibrary="android.support.v14.preference" />
+    <application
+        android:name="it.ao2.savemysugar.SaveMySugarApplication"
+        android:allowBackup="false"
+        android:fullBackupContent="false"
+        android:label="@string/app_name"
+        android:icon="@drawable/ic_launcher"
+        android:theme="@style/MyMaterialTheme">
+        <receiver android:name=".SendFragment$EndCallReceiver" android:process=":remote"/>
+        <activity
+            android:name="MainActivity"
+            android:label="@string/app_name">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+        </activity>
+        <activity
+            android:name=".SettingsActivity"
+            android:label="@string/menu_settings"
+            android:parentActivityName="MainActivity">
+            <!-- Parent activity meta-data to support 4.0 and lower -->
+            <meta-data
+                android:name="android.support.PARENT_ACTIVITY"
+                android:value="MainActivity" />
+        </activity>
+    </application>
+</manifest>
diff --git a/src/main/java/it/ao2/savemysugar/MainActivity.java b/src/main/java/it/ao2/savemysugar/MainActivity.java
new file mode 100644 (file)
index 0000000..9b25498
--- /dev/null
@@ -0,0 +1,161 @@
+/*
+ * SaveMySugar - Exchange messages using the distance between phone calls
+ *
+ * Copyright (C) 2015  Antonio Ospite <ao2@ao2.it>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package it.ao2.savemysugar;
+
+import android.annotation.SuppressLint;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.os.Bundle;
+import android.support.design.widget.TabLayout;
+import android.support.v4.app.Fragment;
+import android.support.v4.app.FragmentManager;
+import android.support.v4.app.FragmentPagerAdapter;
+import android.support.v4.view.ViewPager;
+import android.support.v7.app.AlertDialog;
+import android.support.v7.app.AppCompatActivity;
+import android.support.v7.widget.Toolbar;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.TextView;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class MainActivity extends AppCompatActivity {
+    private static final String TAG = "SaveMySugar";
+    private static final int REQUEST_PREFS = 1;
+
+    /** Called when the activity is first created. */
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.activity_main);
+
+        Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
+        setSupportActionBar(toolbar);
+
+        ViewPagerAdapter adapter = new ViewPagerAdapter(getSupportFragmentManager());
+        adapter.addFragment(new ReceiveFragment(), "Receive");
+        adapter.addFragment(new SendFragment(), "Send");
+
+        ViewPager viewPager = (ViewPager) findViewById(R.id.viewpager);
+        viewPager.setAdapter(adapter);
+
+        TabLayout tabLayout = (TabLayout) findViewById(R.id.tabs);
+        tabLayout.setupWithViewPager(viewPager);
+    }
+
+    static class ViewPagerAdapter extends FragmentPagerAdapter {
+        private final List<Fragment> mFragments = new ArrayList<>();
+        private final List<String> mFragmentTitles = new ArrayList<>();
+
+        ViewPagerAdapter(FragmentManager fm) {
+            super(fm);
+        }
+
+        public void addFragment(Fragment fragment, String title) {
+            mFragments.add(fragment);
+            mFragmentTitles.add(title);
+        }
+
+        @Override
+        public Fragment getItem(int position) {
+            return mFragments.get(position);
+        }
+
+        @Override
+        public int getCount() {
+            return mFragments.size();
+        }
+
+        @Override
+        public CharSequence getPageTitle(int position) {
+            return mFragmentTitles.get(position);
+        }
+    }
+
+    // Workaround to have the generic menu key (either a hardware one or the
+    // one the in bottom navigation bar) open the overflow menu.
+    @Override
+    public boolean onKeyUp(int keyCode, KeyEvent event) {
+        if (keyCode == KeyEvent.KEYCODE_MENU) {
+            Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
+            if (toolbar.isOverflowMenuShowing()) {
+                toolbar.dismissPopupMenus();
+            } else {
+                toolbar.showOverflowMenu();
+            }
+            return true;
+        }
+        return super.onKeyUp(keyCode, event);
+    }
+
+    @Override
+    public boolean onCreateOptionsMenu(Menu menu) {
+        getMenuInflater().inflate(R.menu.main, menu);
+        return true;
+    }
+
+    @Override
+    public boolean onOptionsItemSelected(MenuItem item) {
+        switch (item.getItemId()) {
+
+        case R.id.menu_settings:
+            Intent i = new Intent(this, SettingsActivity.class);
+            startActivityForResult(i, REQUEST_PREFS);
+            return true;
+
+        case R.id.action_about:
+            @SuppressLint("InflateParams")
+            // See the last section in
+            // https://possiblemobile.com/2013/05/layout-inflation-as-intended/
+            View view = LayoutInflater.from(this).inflate(R.layout.about, null);
+
+            ((TextView) view.findViewById(R.id.version)).setText(BuildConfig.VERSION_NAME);
+
+            AlertDialog alrt = new AlertDialog.Builder(this).setView(view).create();
+            alrt.setTitle(R.string.about_title);
+            alrt.setButton(AlertDialog.BUTTON_NEGATIVE, getString(R.string.ok),
+                           new DialogInterface.OnClickListener() {
+                               @Override
+                               public void onClick(DialogInterface dialog, int whichButton) {
+                               }
+                           });
+            alrt.show();
+            return true;
+        }
+
+        return super.onOptionsItemSelected(item);
+    }
+
+    @Override
+    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+        super.onActivityResult(requestCode, resultCode, data);
+
+        switch (requestCode) {
+        case REQUEST_PREFS:
+            ((SaveMySugarApplication) getApplication()).updateModulatorParams();
+            break;
+        }
+    }
+}
diff --git a/src/main/java/it/ao2/savemysugar/SaveMySugarApplication.java b/src/main/java/it/ao2/savemysugar/SaveMySugarApplication.java
new file mode 100644 (file)
index 0000000..cbc773b
--- /dev/null
@@ -0,0 +1,80 @@
+/*
+ * SaveMySugar - Exchange messages using the distance between phone calls
+ *
+ * Copyright (C) 2015  Antonio Ospite <ao2@ao2.it>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * adouble with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package it.ao2.savemysugar;
+
+import android.app.Application;
+import android.content.SharedPreferences;
+import android.preference.PreferenceManager;
+import android.util.Log;
+
+import it.ao2.util.morse.MorseDistanceModulator;
+import it.ao2.util.morse.MorseTranslator;
+
+public class SaveMySugarApplication extends Application {
+
+    private static final String TAG = "SaveMySugar";
+    private MorseDistanceModulator mModulator;
+    private MorseTranslator mTranslator;
+
+    @Override
+    public void onCreate() {
+        super.onCreate();
+
+        mModulator = new MorseDistanceModulator();
+        updateModulatorParams();
+
+        mTranslator = new MorseTranslator();
+    }
+
+    public void updateModulatorParams() {
+        String defaultCallSetupTimeMin = Double.toString(mModulator.DEFAULT_PERIOD_MIN);
+        String defaultCallSetupTimeMax = Double.toString(mModulator.DEFAULT_PERIOD_MAX);
+        String defaultRingTimeMin = Double.toString(mModulator.DEFAULT_PULSE_MIN);
+        String defaultRingTimeMax = Double.toString(mModulator.DEFAULT_PULSE_MAX);
+
+        SharedPreferences sharedPrefs = PreferenceManager.getDefaultSharedPreferences(this);
+        double newCallSetupTimeMin = Double.parseDouble(sharedPrefs.getString("call_setup_time_min", defaultCallSetupTimeMin));
+        double newCallSetupTimeMax = Double.parseDouble(sharedPrefs.getString("call_setup_time_max", defaultCallSetupTimeMax));
+        double newRingTimeMin = Double.parseDouble(sharedPrefs.getString("ring_time_min", defaultRingTimeMin));
+        double newRingTimeMax = Double.parseDouble(sharedPrefs.getString("ring_time_max", defaultRingTimeMax));
+
+        Log.d(TAG, "Call setup time min: " + newCallSetupTimeMin + "\n");
+        Log.d(TAG, "Call setup time max: " + newCallSetupTimeMax + "\n");
+        Log.d(TAG, "Ring time min: " + newRingTimeMin + "\n");
+        Log.d(TAG, "Ring time max: " + newRingTimeMax + "\n");
+
+        // Analog modems don't like to dial a new call immediately after they
+        // hung up the previous one; an extra delay of about one ring time
+        // makes them happy: use newRingTimeMax as the interSymbolDistance.
+        double interSymbolDistance = newRingTimeMax;
+
+        mModulator.setParameters(newCallSetupTimeMin, newCallSetupTimeMax,
+                                 newRingTimeMin, newRingTimeMax,
+                                 interSymbolDistance);
+    }
+
+    public MorseDistanceModulator getModulator() {
+        return mModulator;
+    }
+
+    public MorseTranslator getTranslator() {
+        return mTranslator;
+    }
+}
diff --git a/src/main/java/it/ao2/savemysugar/SettingsActivity.java b/src/main/java/it/ao2/savemysugar/SettingsActivity.java
new file mode 100644 (file)
index 0000000..fb97c52
--- /dev/null
@@ -0,0 +1,51 @@
+/*
+ * SaveMySugar - Exchange messages using the distance between phone calls
+ *
+ * Copyright (C) 2015  Antonio Ospite <ao2@ao2.it>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package it.ao2.savemysugar;
+
+import android.os.Bundle;
+import android.support.v7.app.ActionBar;
+import android.support.v7.app.AppCompatActivity;
+import android.support.v7.preference.PreferenceFragmentCompat;
+import android.support.v7.widget.Toolbar;
+
+public class SettingsActivity extends AppCompatActivity {
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.activity_settings);
+
+        Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
+        setSupportActionBar(toolbar);
+
+        ActionBar ab = getSupportActionBar();
+        ab.setDisplayHomeAsUpEnabled(true);
+
+        getSupportFragmentManager().beginTransaction().replace(R.id.content_wrapper_settings, new MyPreferenceFragment()).commit();
+    }
+
+    public static class MyPreferenceFragment extends PreferenceFragmentCompat {
+
+        @Override
+        public void onCreatePreferences(final Bundle savedInstanceState, String s) {
+            addPreferencesFromResource(R.xml.preferences);
+        }
+    }
+}
diff --git a/src/main/java/it/ao2/savemysugar/fragments/ReceiveFragment.java b/src/main/java/it/ao2/savemysugar/fragments/ReceiveFragment.java
new file mode 100644 (file)
index 0000000..fca6e2b
--- /dev/null
@@ -0,0 +1,152 @@
+/*
+ * SaveMySugar - Exchange messages using the distance between phone calls
+ *
+ * Copyright (C) 2015  Antonio Ospite <ao2@ao2.it>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * adouble with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package it.ao2.savemysugar;
+
+import android.content.Context;
+import android.os.Bundle;
+import android.support.v4.app.Fragment;
+import android.telephony.PhoneStateListener;
+import android.telephony.TelephonyManager;
+import android.text.method.ScrollingMovementMethod;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import java.util.Locale;
+
+import it.ao2.savemysugar.util.TimeLog;
+import it.ao2.util.morse.MorseDistanceModulator;
+import it.ao2.util.morse.MorseTranslator;
+
+public class ReceiveFragment extends Fragment {
+
+    private static final String TAG = "SaveMySugar";
+    private TextView mLogWindow;
+    private MorseDistanceModulator mModulator;
+    private MorseTranslator mTranslator;
+    private SaveMySugarPhoneStateListener mListener;
+
+    private class SaveMySugarPhoneStateListener extends PhoneStateListener {
+
+        private double mPreviousRingTime = -1;
+        private double mPreviousCallTime = -1;
+        private String mMorseMessage = "";
+
+        public void onCallStateChanged(int state, String incomingNumber) {
+            switch (state) {
+            case TelephonyManager.CALL_STATE_IDLE:
+                TimeLog.log(TAG, mLogWindow, "CALL_STATE_IDLE\n");
+                break;
+            case TelephonyManager.CALL_STATE_OFFHOOK:
+                TimeLog.log(TAG, mLogWindow, "CALL_STATE_OFFHOOK\n");
+                break;
+            case TelephonyManager.CALL_STATE_RINGING:
+                TimeLog.log(TAG, mLogWindow, "CALL_STATE_RINGING " + incomingNumber + "\n");
+                double currentRingTime = System.currentTimeMillis() / 1000.;
+
+                if (mPreviousRingTime == -1) {
+                    mPreviousRingTime = currentRingTime;
+                    mPreviousCallTime = currentRingTime;
+                    TimeLog.log(TAG, mLogWindow, "call distance 0 Received \"\" (The very first ring)\n");
+                    break;
+                }
+
+                double ringDistance = currentRingTime - mPreviousRingTime;
+                TimeLog.log(TAG, mLogWindow, "RINGs distance " + String.format(Locale.US, "%.2f", ringDistance) + "\n");
+                mPreviousRingTime = currentRingTime;
+
+                // Ignore multiple rings in the same call
+                if (mModulator.isSamePeriod(ringDistance)) {
+                    TimeLog.log(TAG, mLogWindow, "Multiple rings in the same call, distance: "
+                                +  String.format(Locale.US, "%.2f", ringDistance) + "\n");
+                    break;
+                }
+
+                double callDistance = currentRingTime - mPreviousCallTime;
+                mPreviousCallTime = currentRingTime;
+
+                try {
+                    String symbol =  mModulator.distanceToSymbol(callDistance);
+
+                    String extraInfo = "";
+                    // When a separator is met, log the last Morse signal
+                    if (symbol.equals(" ") || symbol.equals("/") || symbol.equals("EOM")) {
+                        String[] signals = mMorseMessage.split(" ");
+                        String lastSignal = signals[signals.length - 1];
+                        Character lastCharacter = mTranslator.signalToCharacter(lastSignal);
+                        extraInfo = " got \"" + lastCharacter  + "\"";
+                    }
+
+                    TimeLog.log(TAG, mLogWindow, "call distance " + String.format(Locale.US, "%.2f", callDistance)
+                                + " Received \"" + symbol + "\"" + extraInfo + "\n");
+
+                    if (symbol.equals("EOM")) {
+                        String text = mTranslator.morseToText(mMorseMessage);
+                        TimeLog.log(TAG, mLogWindow, "Message received: " + text + "\n");
+                        mMorseMessage = "";
+                    } else {
+                        // Add spaces around the wordspace symbol to make it
+                        // easier to split the Morse message later on
+                        if (symbol.equals("/")) {
+                            symbol = " / ";
+                        }
+                        mMorseMessage += symbol;
+                    }
+                } catch (IllegalArgumentException e) {
+                    Toast.makeText(getActivity(), e.getMessage(), Toast.LENGTH_LONG).show();
+                }
+                break;
+            default:
+                break;
+            }
+            super.onCallStateChanged(state, incomingNumber);
+        }
+    };
+
+    @Override
+    public void onAttach(Context context) {
+        super.onAttach(context);
+        mModulator = ((SaveMySugarApplication) getActivity().getApplication()).getModulator();
+        mTranslator = ((SaveMySugarApplication) getActivity().getApplication()).getTranslator();
+
+        if (mListener == null) {
+            mListener = new SaveMySugarPhoneStateListener();
+
+            TelephonyManager telephonyMgr = (TelephonyManager) getActivity().getSystemService(Context.TELEPHONY_SERVICE);
+            telephonyMgr.listen(mListener, PhoneStateListener.LISTEN_CALL_STATE);
+
+            // If we used a Service this would not be necessary
+            setRetainInstance(true);
+        }
+    }
+
+    @Override
+    public View onCreateView(LayoutInflater inflater, ViewGroup container,
+                             Bundle savedInstanceState) {
+        View view = inflater.inflate(R.layout.fragment_receive, container, false);
+
+        mLogWindow = (TextView) view.findViewById(R.id.receive_log_window);
+        mLogWindow.setMovementMethod(new ScrollingMovementMethod());
+
+        return view;
+    }
+}
diff --git a/src/main/java/it/ao2/savemysugar/fragments/SendFragment.java b/src/main/java/it/ao2/savemysugar/fragments/SendFragment.java
new file mode 100644 (file)
index 0000000..975f061
--- /dev/null
@@ -0,0 +1,236 @@
+/*
+ * SaveMySugar - Exchange messages using the distance between phone calls
+ *
+ * Copyright (C) 2015  Antonio Ospite <ao2@ao2.it>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package it.ao2.savemysugar;
+
+import android.app.AlarmManager;
+import android.app.PendingIntent;
+import android.content.ActivityNotFoundException;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.support.v4.app.Fragment;
+import android.telephony.TelephonyManager;
+import android.text.method.ScrollingMovementMethod;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.TextView;
+
+import java.lang.reflect.Method;
+import java.util.List;
+import java.util.Locale;
+
+import it.ao2.savemysugar.util.TimeLog;
+import it.ao2.util.morse.MorseDistanceModulator;
+import it.ao2.util.morse.MorseTranslator;
+
+public class SendFragment extends Fragment {
+
+    private static final String TAG = "SaveMySugar";
+    private TextView mLogWindow;
+    private String mDestinationNumber;
+    private MorseDistanceModulator mModulator;
+    private List<Double> mDistances;
+    private long mEndCallTime;
+    private Handler mSendSymbolHandler;
+
+    public static class EndCallReceiver extends BroadcastReceiver {
+
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            endCall(context);
+        }
+
+        public void endCall(Context context) {
+            TelephonyManager tm = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
+            try {
+                Class<?> c = Class.forName(tm.getClass().getName());
+                Method m = c.getDeclaredMethod("getITelephony");
+                m.setAccessible(true);
+                Object telephonyService = m.invoke(tm);
+
+                c = Class.forName(telephonyService.getClass().getName());
+                m = c.getDeclaredMethod("endCall");
+                m.setAccessible(true);
+                m.invoke(telephonyService);
+
+            } catch (Exception e) {
+                e.printStackTrace();
+            }
+        }
+    }
+
+    @Override
+    public void onAttach(Context context) {
+        super.onAttach(context);
+        mModulator = ((SaveMySugarApplication) getActivity().getApplication()).getModulator();
+    }
+
+    @Override
+    public View onCreateView(LayoutInflater inflater, ViewGroup container,
+                             Bundle savedInstanceState) {
+        View view = inflater.inflate(R.layout.fragment_send, container, false);
+
+        mLogWindow = (TextView) view.findViewById(R.id.send_log_window);
+        mLogWindow.setMovementMethod(new ScrollingMovementMethod());
+
+        final Button sendMessageButton = (Button) view.findViewById(R.id.send_message);
+        sendMessageButton.setOnClickListener(new View.OnClickListener() {
+            public void onClick(View v) {
+                sendMessage();
+            }
+        });
+
+        return view;
+    }
+
+    @Override
+    public void onDestroy() {
+        super.onDestroy();
+
+        cancelEndCallAlarm();
+        if (mSendSymbolHandler != null) {
+            mSendSymbolHandler.removeCallbacksAndMessages(null);
+        }
+    }
+
+    private void setEndCallAlarm(long timeout) {
+        Intent intent = new Intent(getActivity(), EndCallReceiver.class);
+        PendingIntent pi = PendingIntent.getBroadcast(getActivity().getApplicationContext(), 0, intent, 0);
+
+        AlarmManager alarmManager = (AlarmManager) getActivity().getSystemService(Context.ALARM_SERVICE);
+        alarmManager.set(AlarmManager.RTC_WAKEUP, timeout, pi);
+    }
+
+    private void cancelEndCallAlarm() {
+        Intent intent = new Intent(getActivity(), EndCallReceiver.class);
+        PendingIntent pi = PendingIntent.getBroadcast(getActivity().getApplicationContext(), 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
+
+        AlarmManager alarmManager = (AlarmManager) getActivity().getSystemService(Context.ALARM_SERVICE);
+        alarmManager.cancel(pi);
+    }
+
+    // The function is called sendSymbol() for simplicity, because this is
+    // what it looks like it does to a caller, but it actually just starts
+    // the call, the symbol is represented by the distance _after_ the call
+    // ends and this is handled in onActivityResult().
+    public void sendSymbol(String phoneNumber, int index) {
+        if (!phoneNumber.equals("")) {
+            double callSetupTime = mModulator.getPeriod();
+            double distance = mDistances.get(index);
+            String symbol = (index == mDistances.size() - 1) ? "None" : mModulator.distanceToSymbol(callSetupTime + distance);
+
+            TimeLog.log(TAG, mLogWindow, "sendSymbol Dial and wait "
+                        + String.format(Locale.US, "%.2f", (callSetupTime + distance)) + " = "
+                        + String.format(Locale.US, "%.2f", callSetupTime) + " + "
+                        + String.format(Locale.US, "%.2f", distance) + " seconds "
+                        + "(transmitting '" + symbol + "')\n");
+
+            try {
+                Intent callIntent = new Intent(Intent.ACTION_CALL);
+                callIntent.setData(Uri.parse("tel:" + phoneNumber));
+
+                // Set the alarm right before starting the ACTION_CALL
+                // activity to minimize the risk of a premature hangup.
+                //
+                // If there were interleaving operations between setting the
+                // alarm and starting the call, the call might be ended before
+                // callSetupTime has passed from when the call is actually
+                // placed.
+                mEndCallTime = System.currentTimeMillis() + (long) (callSetupTime * 1000);
+                setEndCallAlarm(mEndCallTime);
+                startActivityForResult(callIntent, index);
+            } catch (ActivityNotFoundException e) {
+                Log.e(TAG, "Call failed", e);
+            }
+        }
+    }
+
+    @Override
+    public void onActivityResult(int requestCode, int resultCode, Intent data) {
+        super.onActivityResult(requestCode, resultCode, data);
+
+        // Here the requestCode value is "abused" to advance in the
+        // transmission process. The logic used here assumes that the very
+        // last distance, the one which terminates the transmission of
+        // a message, can be skipped altogether; after all there's no call
+        // after the last distance anyways, it's there just to make sure that
+        // a final call is made, and sendSymbol() takes care of that.
+        final int nextRequest = requestCode + 1;
+        if (nextRequest < mDistances.size()) {
+            // The timeout set in AlarmManager cannot be blindly trusted, it
+            // may have passed more than callSetupTime seconds since
+            // callIntent started.
+            //
+            // Account for possible delays in order to be as adherent as
+            // possible to the nominal total symbol transmission distance.
+            long delay = System.currentTimeMillis() - mEndCallTime;
+            Log.d(TAG, "Delay " + (delay / 1000.));
+
+            long distance = (long) (mDistances.get(requestCode) * 1000) - delay;
+            if (distance < 0) {
+                distance = 0;
+            }
+
+            Log.d(TAG, "Should sleep " + mDistances.get(requestCode) + ". Will sleep " + (distance / 1000.));
+
+            // This is where the actual symbol transmission happens: in the
+            // distance before the next symbol is sent.
+            mSendSymbolHandler = new Handler();
+            mSendSymbolHandler.postDelayed(new Runnable() {
+                @Override
+                public void run() {
+                    sendSymbol(mDestinationNumber, nextRequest);
+                    mSendSymbolHandler = null;
+                }
+            }, distance);
+        }
+    }
+
+    /** Called when the user clicks the Send button */
+    public void sendMessage() {
+        Log.d(TAG, "sending Message");
+        View view = getView();
+
+        EditText editNumber = (EditText) view.findViewById(R.id.destination_number);
+        mDestinationNumber = editNumber.getText().toString().trim();
+
+        EditText editMessage = (EditText) view.findViewById(R.id.message);
+        String message = editMessage.getText().toString();
+
+        MorseTranslator translator =  ((SaveMySugarApplication) getActivity().getApplication()).getTranslator();
+        String morse = translator.textToMorse(message);
+
+        mDistances = mModulator.modulate(morse);
+
+        mLogWindow.setText(morse);
+        mLogWindow.append("\n");
+
+        if (mDistances.size() > 0) {
+            sendSymbol(mDestinationNumber, 0);
+        }
+    }
+}
diff --git a/src/main/java/it/ao2/savemysugar/morse/MorseDistanceModulator.java b/src/main/java/it/ao2/savemysugar/morse/MorseDistanceModulator.java
new file mode 100644 (file)
index 0000000..892bc36
--- /dev/null
@@ -0,0 +1,175 @@
+/*
+ * Represent Morse code symbols using time intervals
+ *
+ * Copyright (C) 2015  Antonio Ospite <ao2@ao2.it>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package it.ao2.util.morse;
+
+import java.util.List;
+import java.util.Vector;
+
+public class MorseDistanceModulator {
+    public static final double DEFAULT_PERIOD_MIN = 7;
+    public static final double DEFAULT_PERIOD_MAX = 16.5;
+    public static final double DEFAULT_PULSE_MIN = 4.9;
+    public static final double DEFAULT_PULSE_MAX = 5.2;
+    public static final double DEFAULT_INTER_SYMBOL_DISTANCE = DEFAULT_PULSE_MAX;
+
+    private double mPeriodMin;
+    private double mPeriodMax;
+    private double mPulseMin;
+    private double mPulseMax;
+
+    private SymbolTime mDotTime;
+    private SymbolTime mDashTime;
+    private SymbolTime mSignalspaceTime;
+    private SymbolTime mWordspaceTime;
+    private SymbolTime mEOMTime;
+
+    private class SymbolTime {
+        private double dist;
+        private double min;
+        private double max;
+
+        SymbolTime(double periodMin, double periodMax, double multiplier,
+                   double minInterSymbolDistance) {
+            if (multiplier < 0) {
+                throw new IllegalArgumentException("multiplier must me non-negative");
+            }
+            if (periodMin == periodMax && minInterSymbolDistance == 0) {
+                throw new IllegalArgumentException("If (periodMin == periodMax) a non zero "
+                                                   + "inter-symbol distance MUST be specified");
+            }
+
+            double symbolDistance = 2 * (periodMax - periodMin);
+            if (symbolDistance == 0) {
+                symbolDistance = minInterSymbolDistance;
+            }
+
+            dist = minInterSymbolDistance + symbolDistance * multiplier;
+            min = minInterSymbolDistance + periodMin + symbolDistance * multiplier;
+            max = minInterSymbolDistance + periodMin + symbolDistance * (multiplier + 1);
+        }
+
+        SymbolTime(double periodMin, double periodMax, double multiplier) {
+            this(periodMin, periodMax, multiplier, 0.0);
+        }
+
+        public double getDist() {
+            return dist;
+        }
+
+        public double getMin() {
+            return min;
+        }
+
+        public double getMax() {
+            return max;
+        }
+    }
+
+    public MorseDistanceModulator() {
+        setParameters(DEFAULT_PERIOD_MIN,
+                      DEFAULT_PERIOD_MAX,
+                      DEFAULT_PULSE_MIN,
+                      DEFAULT_PULSE_MAX,
+                      DEFAULT_INTER_SYMBOL_DISTANCE);
+    }
+
+    public void setParameters(double newPeriodMin,
+                              double newPeriodMax,
+                              double newPulseMin,
+                              double newPulseMax,
+                              double interSymbolDistance) {
+        mPeriodMin = newPeriodMin;
+        mPeriodMax = newPeriodMax;
+        mPulseMin = newPulseMin;
+        mPulseMax = newPulseMax;
+
+        mDotTime = new SymbolTime(mPeriodMin, mPeriodMax, 0, interSymbolDistance);
+        mDashTime = new SymbolTime(mPeriodMin, mPeriodMax, 1, interSymbolDistance);
+        mSignalspaceTime = new SymbolTime(mPeriodMin, mPeriodMax, 2, interSymbolDistance);
+        mWordspaceTime = new SymbolTime(mPeriodMin, mPeriodMax, 3, interSymbolDistance);
+        mEOMTime = new SymbolTime(mPeriodMin, mPeriodMax, 4, interSymbolDistance);
+    }
+
+    public double getPeriod() {
+        return mPeriodMax;
+    }
+
+    public double symbolToDistance(String symbol) {
+        switch (symbol) {
+        case ".":
+            return mDotTime.getDist();
+        case "-":
+            return mDashTime.getDist();
+        case " ":
+            return mSignalspaceTime.getDist();
+        case "/":
+        case " / ":
+            return mWordspaceTime.getDist();
+        case "EOM":
+            return mEOMTime.getDist();
+        default:
+            throw new IllegalArgumentException("Invalid Morse Symbol: " + symbol);
+        }
+    }
+
+    // Multiple pulses in the same period
+    public boolean isSamePeriod(double pulseDistance) {
+        return (pulseDistance > mPulseMin && pulseDistance <= mPulseMax);
+    }
+
+    public String distanceToSymbol(double distance) {
+        if (distance > mDotTime.getMin() && distance <= mDotTime.getMax()) {
+            return ".";
+        } else if (distance > mDashTime.getMin() && distance <= mDashTime.getMax()) {
+            return "-";
+        } else if (distance > mSignalspaceTime.getMin() && distance <= mSignalspaceTime.getMax()) {
+            return " ";
+        } else if (distance > mWordspaceTime.getMin() && distance <= mWordspaceTime.getMax()) {
+            return "/";
+        } else if (distance > mEOMTime.getMin()) {
+            return "EOM";
+        }
+
+        throw new IllegalArgumentException("Unexpected distance: " + distance);
+    }
+
+    public List<Double> modulate(String morse) {
+        List<Double> distances = new Vector<Double>();
+
+        String[] signals = morse.split(" ");
+        for (int i = 0; i < signals.length; i++) {
+            String signal = signals[i];
+            double distance;
+            for (int j = 0; j < signal.length(); j++) {
+                String symbol = Character.toString(signal.charAt(j));
+                distance = symbolToDistance(symbol);
+                distances.add(distance);
+            }
+            if (i != (signals.length - 1) && !signal.equals("/")  && !signals[i + 1].equals("/")) {
+                distance = symbolToDistance(" ");
+                distances.add(distance);
+            }
+        }
+        distances.add(symbolToDistance("EOM"));
+        distances.add(0.0);
+
+        return distances;
+    }
+}
diff --git a/src/main/java/it/ao2/savemysugar/morse/MorseTranslator.java b/src/main/java/it/ao2/savemysugar/morse/MorseTranslator.java
new file mode 100644 (file)
index 0000000..9fb3ede
--- /dev/null
@@ -0,0 +1,225 @@
+/*
+ * Morse Code Translator Java class
+ *
+ * Copyright (C) 2015  Antonio Ospite <ao2@ao2.it>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * ---------------------------------------------------------------------------
+ *
+ * The code was initially based on:
+ *
+ *   - Morse.java by Stephen C Phillips:
+ *     http://morsecode.scphillips.com/Morse.java
+ *     Copyright (C) 1999-2004  Stephen C Phillips <steve@scphillips.com>
+ *
+ *   - NMorse.java by Michael R Ditto:
+ *     http://www.omnicron.com/~ford/java/NMorse.java
+ *     Copyright (C) 2001  Michael R Ditto <ford@omnicron.com>
+ *
+ * ---------------------------------------------------------------------------
+ *
+ * The specification of the International Morse Code is in ITU-R M.1677-1
+ * (10/2009), Annex 1.
+ *
+ * The terminology used here may differ from the one used in some other
+ * places, so here is some nomenclature:
+ *
+ *     symbol: one of . (dot), - (dash), ' ' (signal separator),
+ *         '/' (word separator)
+ *
+ *     character: a letter of the alphabet, a number, a punctuation mark, or
+ *         a ' ' (text word separator)
+ *
+ *     signal: a sequence of . and - symbols which encode a character,
+ *         or a '/' (Morse word separator)
+ *
+ *     word: a sequence of characters not containing a ' ', or
+ *         a sequence of signals not containing a '/'
+ *
+ *     text: a sequence of characters
+ *
+ *     morse: a sequence of signals separated by ' '
+ *
+ * NOTE:
+ * signals are separated by a ' ' (signal separator), characters are not
+ * separated one from the other.
+ *
+ * This class defines a subset of the signals in Section 1 of the
+ * aforementioned specification, plus a word space, and it does not make
+ * assumptions about their actual transmission.
+ */
+
+package it.ao2.util.morse;
+
+import java.util.Arrays;
+import java.util.Enumeration;
+import java.util.Hashtable;
+import java.util.Iterator;
+import java.util.Locale;
+
+public class MorseTranslator {
+    private Hashtable<Character, String> mSignalsTable;
+    private Hashtable<String, Character> mCharactersTable;
+
+    public MorseTranslator() {
+        mSignalsTable = new Hashtable<Character, String>();
+
+        // Letters
+        mSignalsTable.put(Character.valueOf('a'), ".-");
+        mSignalsTable.put(Character.valueOf('b'), "-...");
+        mSignalsTable.put(Character.valueOf('c'), "-.-.");
+        mSignalsTable.put(Character.valueOf('d'), "-..");
+        mSignalsTable.put(Character.valueOf('e'), ".");
+        mSignalsTable.put(Character.valueOf('f'), "..-.");
+        mSignalsTable.put(Character.valueOf('g'), "--.");
+        mSignalsTable.put(Character.valueOf('h'), "....");
+        mSignalsTable.put(Character.valueOf('i'), "..");
+        mSignalsTable.put(Character.valueOf('j'), ".---");
+        mSignalsTable.put(Character.valueOf('k'), "-.-");
+        mSignalsTable.put(Character.valueOf('l'), ".-..");
+        mSignalsTable.put(Character.valueOf('m'), "--");
+        mSignalsTable.put(Character.valueOf('n'), "-.");
+        mSignalsTable.put(Character.valueOf('o'), "---");
+        mSignalsTable.put(Character.valueOf('p'), ".--.");
+        mSignalsTable.put(Character.valueOf('q'), "--.-");
+        mSignalsTable.put(Character.valueOf('r'), ".-.");
+        mSignalsTable.put(Character.valueOf('s'), "...");
+        mSignalsTable.put(Character.valueOf('t'), "-");
+        mSignalsTable.put(Character.valueOf('u'), "..-");
+        mSignalsTable.put(Character.valueOf('v'), "...-");
+        mSignalsTable.put(Character.valueOf('w'), ".--");
+        mSignalsTable.put(Character.valueOf('x'), "-..-");
+        mSignalsTable.put(Character.valueOf('y'), "-.--");
+        mSignalsTable.put(Character.valueOf('z'), "--..");
+        // Figures
+        mSignalsTable.put(Character.valueOf('1'), ".----");
+        mSignalsTable.put(Character.valueOf('2'), "..---");
+        mSignalsTable.put(Character.valueOf('3'), "...--");
+        mSignalsTable.put(Character.valueOf('4'), "....-");
+        mSignalsTable.put(Character.valueOf('5'), ".....");
+        mSignalsTable.put(Character.valueOf('6'), "-....");
+        mSignalsTable.put(Character.valueOf('7'), "--...");
+        mSignalsTable.put(Character.valueOf('8'), "---..");
+        mSignalsTable.put(Character.valueOf('9'), "----.");
+        mSignalsTable.put(Character.valueOf('0'), "-----");
+        // Punctuation marks and miscellaneous signs
+        mSignalsTable.put(Character.valueOf('.'), ".-.-.-");
+        mSignalsTable.put(Character.valueOf(','), "--..--");
+        mSignalsTable.put(Character.valueOf(':'), "---...");
+        mSignalsTable.put(Character.valueOf('?'), "..--..");
+        mSignalsTable.put(Character.valueOf('\''), ".----.");
+        mSignalsTable.put(Character.valueOf('-'), "-....-");
+        mSignalsTable.put(Character.valueOf('/'), "-..-.");
+        mSignalsTable.put(Character.valueOf('('), "-.--.-");
+        mSignalsTable.put(Character.valueOf(')'), "-.--.-");
+        mSignalsTable.put(Character.valueOf('"'), ".-..-.");
+        mSignalsTable.put(Character.valueOf('='), "-...-");
+        mSignalsTable.put(Character.valueOf('+'), ".-.-.");
+        mSignalsTable.put(Character.valueOf('x'), "-..-");
+        mSignalsTable.put(Character.valueOf('@'), ".--.-.");
+
+        // Represent the word space as a signal with only one "/" symbol
+        mSignalsTable.put(Character.valueOf(' '), "/");
+
+        mCharactersTable = new Hashtable<String, Character>();
+
+        Enumeration<Character> list = mSignalsTable.keys();
+        while (list.hasMoreElements()) {
+            Character c = list.nextElement();
+            String s = mSignalsTable.get(c);
+            mCharactersTable.put(s, c);
+        }
+    }
+
+    public String sanitizeText(String text) {
+        String sanitizedText;
+
+        sanitizedText = text.toLowerCase(Locale.US);
+        sanitizedText = sanitizedText.replaceAll("[^a-z0-9.,:?\'/()\"=+;_$@ -]", "");
+        sanitizedText = sanitizedText.replaceAll("\\s+", " ");
+        return sanitizedText;
+    }
+
+    public String charToSignal(Character character) {
+        character = Character.toLowerCase(character);
+
+        if (mSignalsTable.containsKey(character)) {
+            return mSignalsTable.get(character);
+        } else {
+            return "";
+        }
+    }
+
+    public String textToMorse(String text, boolean sanitize) {
+        if (sanitize) {
+            text = sanitizeText(text);
+        }
+
+        StringBuffer morse = new StringBuffer();
+        for (int i = 0; i < text.length(); i++) {
+            //Character ch = Character.valueOf(text.charAt(i));
+            Character character = text.charAt(i);
+
+            morse.append(charToSignal(character));
+
+            if (i < text.length() - 1) {
+                morse.append(" ");
+            }
+        }
+
+        return morse.toString();
+    }
+
+    public String textToMorse(String text) {
+        return textToMorse(text, true);
+    }
+
+    public String sanitizeMorse(String morse) {
+        String sanitizedMorse;
+
+        sanitizedMorse = morse.replaceAll("_", "-");
+        sanitizedMorse = sanitizedMorse.replaceAll("[^\\-\\.\\/]", " ");
+        sanitizedMorse = sanitizedMorse.replaceAll("\\s+", " ");
+        sanitizedMorse = sanitizedMorse.replaceAll("( ?/ ?)+", " / ");
+        return sanitizedMorse;
+    }
+
+    public Character signalToCharacter(String signal) {
+        if (mCharactersTable.containsKey(signal)) {
+            return mCharactersTable.get(signal);
+        } else {
+            return '*';
+        }
+    }
+
+    public String morseToText(String morse, boolean sanitize) {
+        if (sanitize) {
+            morse = sanitizeMorse(morse);
+        }
+
+        StringBuffer text = new StringBuffer();
+        Iterator<String> signals = Arrays.asList(morse.split(" ")).iterator();
+        while (signals.hasNext()) {
+            String signal = signals.next();
+            text.append(signalToCharacter(signal));
+        }
+
+        return text.toString();
+    }
+
+    public String morseToText(String morse) {
+        return morseToText(morse, true);
+    }
+}
diff --git a/src/main/java/it/ao2/savemysugar/util/TimeLog.java b/src/main/java/it/ao2/savemysugar/util/TimeLog.java
new file mode 100644 (file)
index 0000000..e921f7e
--- /dev/null
@@ -0,0 +1,37 @@
+/*
+ * SaveMySugar - Exchange messages using the distance between phone calls
+ *
+ * Copyright (C) 2015  Antonio Ospite <ao2@ao2.it>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * adouble with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package it.ao2.savemysugar.util;
+
+import android.util.Log;
+import android.widget.TextView;
+
+import java.util.Locale;
+
+public final class TimeLog {
+    private TimeLog() {
+    }
+
+    public static void log(String tag, TextView textView, String message) {
+        double now = System.currentTimeMillis() / 1000.;
+        String nowString =  String.format(Locale.US, "%.6f", now);
+        textView.append(nowString + " " + message);
+        Log.d(tag, nowString + " " + message);
+    }
+}
diff --git a/src/main/res/drawable-hdpi/ic_launcher.png b/src/main/res/drawable-hdpi/ic_launcher.png
new file mode 100644 (file)
index 0000000..f1d881a
Binary files /dev/null and b/src/main/res/drawable-hdpi/ic_launcher.png differ
diff --git a/src/main/res/drawable-ldpi/ic_launcher.png b/src/main/res/drawable-ldpi/ic_launcher.png
new file mode 100644 (file)
index 0000000..b62cdb5
Binary files /dev/null and b/src/main/res/drawable-ldpi/ic_launcher.png differ
diff --git a/src/main/res/drawable-mdpi/ic_launcher.png b/src/main/res/drawable-mdpi/ic_launcher.png
new file mode 100644 (file)
index 0000000..d18763a
Binary files /dev/null and b/src/main/res/drawable-mdpi/ic_launcher.png differ
diff --git a/src/main/res/drawable-xhdpi/ic_launcher.png b/src/main/res/drawable-xhdpi/ic_launcher.png
new file mode 100644 (file)
index 0000000..101a4d3
Binary files /dev/null and b/src/main/res/drawable-xhdpi/ic_launcher.png differ
diff --git a/src/main/res/drawable-xxhdpi/ic_launcher.png b/src/main/res/drawable-xxhdpi/ic_launcher.png
new file mode 100644 (file)
index 0000000..b42575a
Binary files /dev/null and b/src/main/res/drawable-xxhdpi/ic_launcher.png differ
diff --git a/src/main/res/drawable-xxxhdpi/ic_launcher.png b/src/main/res/drawable-xxxhdpi/ic_launcher.png
new file mode 100644 (file)
index 0000000..c822775
Binary files /dev/null and b/src/main/res/drawable-xxxhdpi/ic_launcher.png differ
diff --git a/src/main/res/layout/about.xml b/src/main/res/layout/about.xml
new file mode 100644 (file)
index 0000000..13fca15
--- /dev/null
@@ -0,0 +1,121 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="fill_parent"
+    android:layout_height="fill_parent"
+    android:paddingLeft="24dp"
+    android:paddingRight="24dp"
+    android:paddingEnd="24dp"
+    android:paddingTop="20dp"
+    android:baselineAligned="false"
+    android:orientation="vertical">
+
+    <LinearLayout
+        android:layout_width="fill_parent"
+        android:layout_height="fill_parent"
+        android:orientation="horizontal">
+
+        <TextView
+            android:text="@string/about_version"
+            android:textStyle="bold"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content" />
+
+        <TextView android:id="@+id/version"
+            android:paddingLeft="8sp"
+            android:paddingRight="8sp"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content" />
+
+    </LinearLayout>
+
+    <LinearLayout
+        android:layout_width="fill_parent"
+        android:layout_height="fill_parent"
+        android:orientation="horizontal">
+
+        <TextView
+            android:text="@string/about_source"
+            android:textStyle="bold"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content" />
+
+        <TextView
+            android:text="@string/source_link"
+            android:paddingLeft="8sp"
+            android:paddingRight="8sp"
+            android:autoLink="web"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content" />
+
+    </LinearLayout>
+
+    <LinearLayout
+        android:layout_width="fill_parent"
+        android:layout_height="fill_parent"
+        android:orientation="horizontal">
+
+        <TextView
+            android:text="@string/about_author_name"
+            android:textStyle="bold"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content" />
+
+        <TextView
+            android:text="@string/author_name"
+            android:paddingLeft="8sp"
+            android:paddingRight="8sp"
+            android:autoLink="web"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content" />
+
+    </LinearLayout>
+
+    <LinearLayout
+        android:layout_width="fill_parent"
+        android:layout_height="fill_parent"
+        android:orientation="horizontal">
+
+        <TextView
+            android:text="@string/about_author_website"
+            android:textStyle="bold"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content" />
+
+        <TextView
+            android:text="@string/author_website"
+            android:paddingLeft="8sp"
+            android:paddingRight="8sp"
+            android:autoLink="web"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content" />
+
+    </LinearLayout>
+
+    <LinearLayout
+        android:layout_width="fill_parent"
+        android:layout_height="fill_parent"
+        android:orientation="horizontal">
+
+        <TextView
+            android:text="@string/about_author_email"
+            android:textStyle="bold"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content" />
+
+        <TextView
+            android:text="@string/author_email"
+            android:paddingLeft="8sp"
+            android:paddingRight="8sp"
+            android:autoLink="email"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content" />
+
+    </LinearLayout>
+
+    <TextView
+        android:text="@string/about_desc"
+        android:paddingTop="12dp"
+        android:layout_width="fill_parent"
+        android:layout_height="fill_parent" />
+
+</LinearLayout>
diff --git a/src/main/res/layout/activity_main.xml b/src/main/res/layout/activity_main.xml
new file mode 100644 (file)
index 0000000..c704212
--- /dev/null
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<android.support.design.widget.CoordinatorLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+
+    <android.support.design.widget.AppBarLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:elevation="4dp"
+        android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">
+
+        <include
+            layout="@layout/toolbar" />
+
+        <android.support.design.widget.TabLayout
+            android:id="@+id/tabs"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            app:tabMaxWidth="@dimen/tab_max_width"
+            app:tabMode="fixed"
+            app:tabGravity="fill" />
+
+    </android.support.design.widget.AppBarLayout>
+
+    <android.support.v4.view.ViewPager
+        android:id="@+id/viewpager"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        app:layout_behavior="@string/appbar_scrolling_view_behavior"
+        android:paddingLeft="@dimen/activity_horizontal_margin"
+        android:paddingRight="@dimen/activity_horizontal_margin"
+        android:paddingTop="@dimen/activity_vertical_margin"
+        android:paddingBottom="@dimen/activity_vertical_margin" />
+
+</android.support.design.widget.CoordinatorLayout>
diff --git a/src/main/res/layout/activity_settings.xml b/src/main/res/layout/activity_settings.xml
new file mode 100644 (file)
index 0000000..ca412d0
--- /dev/null
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<android.support.design.widget.CoordinatorLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+
+    <android.support.design.widget.AppBarLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:elevation="4dp"
+        android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">
+
+        <include
+            layout="@layout/toolbar" />
+
+    </android.support.design.widget.AppBarLayout>
+
+    <FrameLayout
+        android:id="@+id/content_wrapper_settings"
+        app:layout_behavior="@string/appbar_scrolling_view_behavior"
+        android:layout_width="match_parent"
+        android:layout_height="fill_parent"
+        android:layout_weight="1" />
+
+</android.support.design.widget.CoordinatorLayout>
diff --git a/src/main/res/layout/fragment_receive.xml b/src/main/res/layout/fragment_receive.xml
new file mode 100644 (file)
index 0000000..9d5b8f6
--- /dev/null
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<RelativeLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="fill_parent"
+    android:layout_height="fill_parent">
+
+    <TextView android:id="@+id/receive_log_window"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:scrollbars = "vertical"
+        android:gravity="bottom"
+        android:layout_alignParentLeft="true" />
+
+</RelativeLayout>
diff --git a/src/main/res/layout/fragment_send.xml b/src/main/res/layout/fragment_send.xml
new file mode 100644 (file)
index 0000000..285dfcf
--- /dev/null
@@ -0,0 +1,52 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<RelativeLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="fill_parent"
+    android:layout_height="fill_parent">
+
+    <android.support.design.widget.TextInputLayout
+        android:id="@+id/input_layout_message"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content">
+
+        <EditText android:id="@+id/message"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:hint="@string/message_hint"
+            android:inputType="textShortMessage" />
+
+    </android.support.design.widget.TextInputLayout>
+
+    <android.support.design.widget.TextInputLayout
+        android:id="@+id/input_layout_destination_number"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:layout_below="@id/input_layout_message"
+        android:layout_toLeftOf="@+id/send_message"
+        android:layout_alignParentLeft="true">
+
+        <EditText android:id="@+id/destination_number"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:inputType="phone"
+            android:digits="0123456789+"
+            android:hint="@string/destination_number_hint" />
+
+    </android.support.design.widget.TextInputLayout>
+
+    <Button android:id="@id/send_message"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:text="@string/button_send"
+        android:layout_below="@id/input_layout_message"
+        android:layout_alignParentRight="true" />
+
+    <TextView android:id="@+id/send_log_window"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:scrollbars = "vertical"
+        android:gravity="bottom"
+        android:layout_below="@id/send_message"
+        android:layout_alignParentLeft="true" />
+
+</RelativeLayout>
diff --git a/src/main/res/layout/toolbar.xml b/src/main/res/layout/toolbar.xml
new file mode 100644 (file)
index 0000000..e72c11b
--- /dev/null
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<android.support.v7.widget.Toolbar
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:id="@+id/toolbar"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    app:popupTheme="@style/ThemeOverlay.AppCompat.Light" />
diff --git a/src/main/res/menu/main.xml b/src/main/res/menu/main.xml
new file mode 100644 (file)
index 0000000..27957cf
--- /dev/null
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<menu
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto">
+
+    <item
+        android:id="@+id/menu_settings"
+        app:showAsAction="never"
+        android:orderInCategory="100"
+        android:title="@string/menu_settings"
+        android:icon="@android:drawable/ic_menu_preferences" />
+
+    <item
+        android:id="@+id/action_about"
+        app:showAsAction="never"
+        android:orderInCategory="110"
+        android:title="@string/menu_about"
+        android:icon="@android:drawable/ic_menu_help" />
+
+</menu>
diff --git a/src/main/res/values-v14/styles.xml b/src/main/res/values-v14/styles.xml
new file mode 100644 (file)
index 0000000..4465b7a
--- /dev/null
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools">
+    <style name="MyMaterialTheme" parent="MyMaterialTheme.Base" />
+
+    <style name="MyMaterialTheme.Base" parent="Theme.AppCompat.Light.NoActionBar">
+        <item name="colorAccent">@color/colorAccent</item>
+        <item name="colorPrimary">@color/colorPrimary</item>
+        <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
+        <item name="android:textColor">@color/textColor</item>
+        <item name="android:textColorPrimary">@color/textColorPrimary</item>
+        <item name="android:windowBackground">@color/windowBackground</item>
+
+        <item name="preferenceTheme">@style/PreferenceThemeOverlay.v14.Material</item>
+
+        <!-- Fix Preferences padding,
+             see https://code.google.com/p/android/issues/detail?id=193986
+
+             Restrict the inflation only to api 17+ otherwise lint gives these errors:
+
+             NewApi: Calling new methods on older versions
+                android:listPreferredItemPaddingStart requires API level 17 (current min is 11)
+                android:listPreferredItemPaddingEnd requires API level 17 (current min is 11)
+
+             Moreover, use local values because otherwise lint gives these warnings:
+
+             PrivateResource: Using private resources
+                The resource @attr/listPreferredItemPaddingLeft is marked as private in com.android.support:design
+                The resource @attr/listPreferredItemPaddingRight is marked as private in com.android.support:design
+             -->
+        <item tools:targetApi="17" name="android:listPreferredItemPaddingStart">@dimen/compat_list_preferred_item_padding_left</item>
+        <item tools:targetApi="17" name="android:listPreferredItemPaddingEnd">@dimen/compat_list_preferred_item_padding_right</item>
+    </style>
+</resources>
diff --git a/src/main/res/values-v21/styles.xml b/src/main/res/values-v21/styles.xml
new file mode 100644 (file)
index 0000000..36f02a2
--- /dev/null
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources>
+    <style name="MyMaterialTheme" parent="MyMaterialTheme.Base">
+        <item name="android:windowContentTransitions">true</item>
+        <item name="android:windowAllowEnterTransitionOverlap">true</item>
+        <item name="android:windowAllowReturnTransitionOverlap">true</item>
+        <item name="android:windowSharedElementEnterTransition">@android:transition/move</item>
+        <item name="android:windowSharedElementExitTransition">@android:transition/move</item>
+    </style>
+</resources>
diff --git a/src/main/res/values/colors.xml b/src/main/res/values/colors.xml
new file mode 100644 (file)
index 0000000..d4734db
--- /dev/null
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources>
+    <color name="colorAccent">#DD2C00</color>
+    <color name="colorPrimary">#1976D2</color>
+    <color name="colorPrimaryDark">#0D47A1</color>
+    <color name="textColor">#2E3436</color>
+    <color name="textColorPrimary">#2E3436</color>
+    <color name="windowBackground">#EEEEEC</color>
+
+    <!-- Fix Preferences header color,
+         see https://code.google.com/p/android/issues/detail?id=193985 -->
+    <color name="preference_fallback_accent_color">@color/colorAccent</color>
+</resources>
diff --git a/src/main/res/values/compat.xml b/src/main/res/values/compat.xml
new file mode 100644 (file)
index 0000000..80c65d4
--- /dev/null
@@ -0,0 +1,5 @@
+<resources>
+    <!-- Backward-compatibility values -->
+    <dimen name="compat_list_preferred_item_padding_left">16dp</dimen>
+    <dimen name="compat_list_preferred_item_padding_right">16dp</dimen>
+</resources>
diff --git a/src/main/res/values/dimens.xml b/src/main/res/values/dimens.xml
new file mode 100644 (file)
index 0000000..9d03e97
--- /dev/null
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources>
+    <!-- Default screen margins, per the Android Design guidelines. -->
+    <dimen name="activity_horizontal_margin">16dp</dimen>
+    <dimen name="activity_vertical_margin">16dp</dimen>
+    <dimen name="tab_max_width">0dp</dimen>
+</resources>
diff --git a/src/main/res/values/donottranslate.xml b/src/main/res/values/donottranslate.xml
new file mode 100644 (file)
index 0000000..483564a
--- /dev/null
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources>
+    <string name="source_link">http://git.ao2.it/SaveMySugar/android-savemysugar.git/</string>
+    <string name="author_name">Antonio Ospite</string>
+    <string name="author_website">http://savemysugar.ao2.it</string>
+    <string name="author_email">ao2@ao2.it</string>
+</resources>
diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml
new file mode 100644 (file)
index 0000000..e32cb9f
--- /dev/null
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools">
+    <string name="message_hint">Message</string>
+    <string name="destination_number_hint">Destination number</string>
+    <string name="button_send">Send</string>
+
+    <string name="menu_settings">Settings</string>
+    <string name="prefs_category_timing_title">Timing</string>
+    <string name="prefs_call_setup_time_min_title">Minimum call setup time</string>
+    <string name="prefs_call_setup_time_min_summary">The minimum possible interval of time between placing a call and receiving the first ring, in seconds</string>
+    <string name="prefs_call_setup_time_max_title">Maximum call setup time</string>
+    <string name="prefs_call_setup_time_max_summary">The maximum possible interval of time between placing a call and receiving the first ring, in seconds</string>
+    <string name="prefs_ring_time_min_title">Minimum ring distance</string>
+    <string name="prefs_ring_time_min_summary">The minimum distance between two consecutive rings in the same call, in seconds</string>
+    <string name="prefs_ring_time_max_title">Maximum ring distance</string>
+    <string name="prefs_ring_time_max_summary">The maximum distance between two consecutive rings in the same call, in seconds</string>
+
+    <string name="menu_about">About</string>
+    <string name="about_title">About SaveMySugar</string>
+    <string name="about_version">Version:</string>
+    <string name="about_source">Source:</string>
+    <string name="about_author_name">Author:</string>
+    <string name="about_author_website">Website:</string>
+    <string name="about_author_email">E-Mail:</string>
+    <string name="about_desc">From an idea by Corrado Rubera.\nReleased under the GNU GPLv3 license.\n</string>
+
+    <string name="ok">OK</string>
+</resources>
diff --git a/src/main/res/values/styles.xml b/src/main/res/values/styles.xml
new file mode 100644 (file)
index 0000000..77ef9d6
--- /dev/null
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources>
+    <style name="MyMaterialTheme" parent="MyMaterialTheme.Base" />
+
+    <style name="MyMaterialTheme.Base" parent="Theme.AppCompat.Light.NoActionBar">
+        <item name="colorAccent">@color/colorAccent</item>
+        <item name="colorPrimary">@color/colorPrimary</item>
+        <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
+        <item name="android:textColor">@color/textColor</item>
+        <item name="android:textColorPrimary">@color/textColorPrimary</item>
+        <item name="android:windowBackground">@color/windowBackground</item>
+
+        <item name="preferenceTheme">@style/PreferenceThemeOverlay</item>
+    </style>
+</resources>
diff --git a/src/main/res/xml/preferences.xml b/src/main/res/xml/preferences.xml
new file mode 100644 (file)
index 0000000..09267e1
--- /dev/null
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
+
+    <PreferenceCategory
+        android:key="timing"
+        android:title="@string/prefs_category_timing_title">
+
+        <EditTextPreference
+            android:key="call_setup_time_min"
+            android:title="@string/prefs_call_setup_time_min_title"
+            android:summary="@string/prefs_call_setup_time_min_summary"
+            android:inputType="number|numberDecimal"
+            android:defaultValue="7"
+            />
+
+        <EditTextPreference
+            android:key="call_setup_time_max"
+            android:title="@string/prefs_call_setup_time_max_title"
+            android:summary="@string/prefs_call_setup_time_max_summary"
+            android:inputType="number|numberDecimal"
+            android:defaultValue="16.5"
+            />
+
+        <EditTextPreference
+            android:key="ring_time_min"
+            android:title="@string/prefs_ring_time_min_title"
+            android:summary="@string/prefs_ring_time_min_summary"
+            android:inputType="number|numberDecimal"
+            android:defaultValue="4.9"
+            />
+
+        <EditTextPreference
+            android:key="ring_time_max"
+            android:title="@string/prefs_ring_time_max_title"
+            android:summary="@string/prefs_ring_time_max_summary"
+            android:inputType="number|numberDecimal"
+            android:defaultValue="5.2"
+            />
+
+    </PreferenceCategory>
+
+</PreferenceScreen>