From: Antonio Ospite Date: Mon, 4 Jan 2016 11:51:55 +0000 (+0100) Subject: Initial import X-Git-Url: https://git.ao2.it/SaveMySugar/android-savemysugar.git/commitdiff_plain/0633aab947fcba6ead913534e4be90a7dcc5aca1 Initial import --- 0633aab947fcba6ead913534e4be90a7dcc5aca1 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bee0491 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.gradle/ +build +local.properties diff --git a/TODO b/TODO new file mode 100644 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 index 0000000..f778d02 --- /dev/null +++ b/artwork/ic_launcher.svg @@ -0,0 +1,190 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..dcb3b1a --- /dev/null +++ b/build.gradle @@ -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 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 index 0000000..179dd79 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -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 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 index 0000000..aec9973 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,90 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windowz variants + +if not "%OS%" == "Windows_NT" goto win9xME_args +if "%@eval[2+2]" == "4" goto 4NT_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* +goto execute + +:4NT_args +@rem Get arguments from the 4NT Shell from JP Software +set CMD_LINE_ARGS=%$ + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/src/androidTest/java/it/ao2/savemysugar/MainActivityTest.java b/src/androidTest/java/it/ao2/savemysugar/MainActivityTest.java new file mode 100644 index 0000000..b7571a1 --- /dev/null +++ b/src/androidTest/java/it/ao2/savemysugar/MainActivityTest.java @@ -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. + *

+ * 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 { + + public MainActivityTest() { + super("it.ao2.savemysugar", MainActivity.class); + } + +} diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml new file mode 100644 index 0000000..eed1ffd --- /dev/null +++ b/src/main/AndroidManifest.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + diff --git a/src/main/java/it/ao2/savemysugar/MainActivity.java b/src/main/java/it/ao2/savemysugar/MainActivity.java new file mode 100644 index 0000000..9b25498 --- /dev/null +++ b/src/main/java/it/ao2/savemysugar/MainActivity.java @@ -0,0 +1,161 @@ +/* + * SaveMySugar - Exchange messages using the distance between phone calls + * + * Copyright (C) 2015 Antonio Ospite + * + * 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 . + */ + +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 mFragments = new ArrayList<>(); + private final List 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 index 0000000..cbc773b --- /dev/null +++ b/src/main/java/it/ao2/savemysugar/SaveMySugarApplication.java @@ -0,0 +1,80 @@ +/* + * SaveMySugar - Exchange messages using the distance between phone calls + * + * Copyright (C) 2015 Antonio Ospite + * + * 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 . + */ + +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 index 0000000..fb97c52 --- /dev/null +++ b/src/main/java/it/ao2/savemysugar/SettingsActivity.java @@ -0,0 +1,51 @@ +/* + * SaveMySugar - Exchange messages using the distance between phone calls + * + * Copyright (C) 2015 Antonio Ospite + * + * 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 . + */ + +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 index 0000000..fca6e2b --- /dev/null +++ b/src/main/java/it/ao2/savemysugar/fragments/ReceiveFragment.java @@ -0,0 +1,152 @@ +/* + * SaveMySugar - Exchange messages using the distance between phone calls + * + * Copyright (C) 2015 Antonio Ospite + * + * 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 . + */ + +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 index 0000000..975f061 --- /dev/null +++ b/src/main/java/it/ao2/savemysugar/fragments/SendFragment.java @@ -0,0 +1,236 @@ +/* + * SaveMySugar - Exchange messages using the distance between phone calls + * + * Copyright (C) 2015 Antonio Ospite + * + * 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 . + */ + +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 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 index 0000000..892bc36 --- /dev/null +++ b/src/main/java/it/ao2/savemysugar/morse/MorseDistanceModulator.java @@ -0,0 +1,175 @@ +/* + * Represent Morse code symbols using time intervals + * + * Copyright (C) 2015 Antonio Ospite + * + * 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 . + */ + +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 modulate(String morse) { + List distances = new Vector(); + + 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 index 0000000..9fb3ede --- /dev/null +++ b/src/main/java/it/ao2/savemysugar/morse/MorseTranslator.java @@ -0,0 +1,225 @@ +/* + * Morse Code Translator Java class + * + * Copyright (C) 2015 Antonio Ospite + * + * 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 . + * + * --------------------------------------------------------------------------- + * + * 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 + * + * - NMorse.java by Michael R Ditto: + * http://www.omnicron.com/~ford/java/NMorse.java + * Copyright (C) 2001 Michael R Ditto + * + * --------------------------------------------------------------------------- + * + * 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 mSignalsTable; + private Hashtable mCharactersTable; + + public MorseTranslator() { + mSignalsTable = new Hashtable(); + + // 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(); + + Enumeration 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 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 index 0000000..e921f7e --- /dev/null +++ b/src/main/java/it/ao2/savemysugar/util/TimeLog.java @@ -0,0 +1,37 @@ +/* + * SaveMySugar - Exchange messages using the distance between phone calls + * + * Copyright (C) 2015 Antonio Ospite + * + * 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 . + */ + +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 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 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 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 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 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 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 index 0000000..13fca15 --- /dev/null +++ b/src/main/res/layout/about.xml @@ -0,0 +1,121 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/res/layout/activity_main.xml b/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..c704212 --- /dev/null +++ b/src/main/res/layout/activity_main.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + diff --git a/src/main/res/layout/activity_settings.xml b/src/main/res/layout/activity_settings.xml new file mode 100644 index 0000000..ca412d0 --- /dev/null +++ b/src/main/res/layout/activity_settings.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + diff --git a/src/main/res/layout/fragment_receive.xml b/src/main/res/layout/fragment_receive.xml new file mode 100644 index 0000000..9d5b8f6 --- /dev/null +++ b/src/main/res/layout/fragment_receive.xml @@ -0,0 +1,14 @@ + + + + + + diff --git a/src/main/res/layout/fragment_send.xml b/src/main/res/layout/fragment_send.xml new file mode 100644 index 0000000..285dfcf --- /dev/null +++ b/src/main/res/layout/fragment_send.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + +