--- /dev/null
+.gradle/
+build
+local.properties
--- /dev/null
+- 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
--- /dev/null
+<?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>
--- /dev/null
+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"
+ }
+}
--- /dev/null
+#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
--- /dev/null
+#!/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 "$@"
--- /dev/null
+@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
--- /dev/null
+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);
+ }
+
+}
--- /dev/null
+<?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>
--- /dev/null
+/*
+ * 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;
+ }
+ }
+}
--- /dev/null
+/*
+ * 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;
+ }
+}
--- /dev/null
+/*
+ * 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);
+ }
+ }
+}
--- /dev/null
+/*
+ * 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;
+ }
+}
--- /dev/null
+/*
+ * 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);
+ }
+ }
+}
--- /dev/null
+/*
+ * 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;
+ }
+}
--- /dev/null
+/*
+ * 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);
+ }
+}
--- /dev/null
+/*
+ * 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);
+ }
+}
--- /dev/null
+<?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>
--- /dev/null
+<?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>
--- /dev/null
+<?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>
--- /dev/null
+<?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>
--- /dev/null
+<?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>
--- /dev/null
+<?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" />
--- /dev/null
+<?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>
--- /dev/null
+<?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>
--- /dev/null
+<?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>
--- /dev/null
+<?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>
--- /dev/null
+<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>
--- /dev/null
+<?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>
--- /dev/null
+<?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>
--- /dev/null
+<?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>
--- /dev/null
+<?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>
--- /dev/null
+<?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>