5
0
mirror of https://github.com/cwinfo/envayasms.git synced 2024-12-04 20:45:32 +00:00

add support for incoming call notifications; fix intermittent NullPointerException when receiving MMS; fix MMS parts with missing filename; allow server to send error messages to be shown in app log (requires updating server library and using EnvayaSMS::get_error_xml()); send device manufacturer/model/sdk version in HTTP User-Agent header; add device_status notification for send_limit_exceeded; add send_status constant for cancelled messages; 30 sec timeouts for outgoing messages; send network type (MOBILE or WIFI) to server; send age (i.e. delay) of incoming message to server; fix crashing bug with checking connectivity; save MMS parts in example server

This commit is contained in:
Jesse Young 2012-03-15 17:20:23 -07:00
parent d93ff76231
commit f53ccc3cc9
37 changed files with 1527 additions and 775 deletions

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
local.properties
bin
gen
nbandroid

View File

@ -1,8 +1,8 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.envaya.sms" package="org.envaya.sms"
android:versionCode="17" android:versionCode="18"
android:versionName="2.0.4"> android:versionName="2.0.5">
<uses-sdk android:minSdkVersion="4" /> <uses-sdk android:minSdkVersion="4" />
@ -10,6 +10,7 @@
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" /> <uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" /> <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" /> <uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.RECEIVE_SMS" /> <uses-permission android:name="android.permission.RECEIVE_SMS" />
<uses-permission android:name="android.permission.RECEIVE_MMS" /> <uses-permission android:name="android.permission.RECEIVE_MMS" />
<uses-permission android:name="android.permission.SEND_SMS" /> <uses-permission android:name="android.permission.SEND_SMS" />
@ -22,7 +23,8 @@
<application android:name="org.envaya.sms.App" <application android:name="org.envaya.sms.App"
android:icon="@drawable/icon" android:label="@string/app_name"> android:icon="@drawable/icon" android:label="@string/app_name">
<activity android:name=".ui.Main" android:label="@string/app_name">
<activity android:name=".ui.LogView" android:label="@string/app_name">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
@ -81,6 +83,9 @@
<receiver android:name=".receiver.DequeueOutgoingMessageReceiver"> <receiver android:name=".receiver.DequeueOutgoingMessageReceiver">
</receiver> </receiver>
<receiver android:name=".receiver.OutgoingMessageTimeout">
</receiver>
<receiver android:name=".receiver.OutgoingMessagePoller"> <receiver android:name=".receiver.OutgoingMessagePoller">
</receiver> </receiver>

17
ant.properties Normal file
View File

@ -0,0 +1,17 @@
# This file is used to override default values used by the Ant build system.
#
# This file must be checked in Version Control Systems, as it is
# integral to the build system of your project.
# This file is only used by the Ant script.
# You can use this to override default values such as
# 'source.dir' for the location of your java source folder and
# 'out.dir' for the location of your output folder.
# You can also use it define how the release builds are signed by declaring
# the following properties:
# 'key.store' for the location of your keystore and
# 'key.alias' for the name of the key to use.
# The password will be asked during the build when you use the 'release' target.

View File

@ -1,17 +0,0 @@
# This file is used to override default values used by the Ant build system.
#
# This file must be checked in Version Control Systems, as it is
# integral to the build system of your project.
# This file is only used by the Ant script.
# You can use this to override default values such as
# 'source.dir' for the location of your java source folder and
# 'out.dir' for the location of your output folder.
# You can also use it define how the release builds are signed by declaring
# the following properties:
# 'key.store' for the location of your keystore and
# 'key.alias' for the name of the key to use.
# The password will be asked during the build when you use the 'release' target.

View File

@ -1,15 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project name="EnvayaSMS" default="help"> <project name="EnvayaSMS" default="help">
<!-- The local.properties file is created and updated by the 'android' <!-- The local.properties file is created and updated by the 'android' tool.
tool.
It contains the path to the SDK. It should *NOT* be checked into It contains the path to the SDK. It should *NOT* be checked into
Version Control Systems. --> Version Control Systems. -->
<property file="local.properties" /> <property file="local.properties" />
<!-- The build.properties file can be created by you and is never touched <!-- The ant.properties file can be created by you. It is only edited by the
by the 'android' tool. This is the place to change some of the 'android' tool to add properties to it.
default property values used by the Ant rules. This is the place to change some Ant specific build properties.
Here are some properties you may want to change/update: Here are some properties you may want to change/update:
source.dir source.dir
@ -17,6 +16,9 @@
out.dir out.dir
The name of the output directory. Default is 'bin'. The name of the output directory. Default is 'bin'.
For other overridable properties, look at the beginning of the rules
files in the SDK, at tools/ant/build.xml
Properties related to the SDK location or the project target should Properties related to the SDK location or the project target should
be updated using the 'android' tool with the 'update' action. be updated using the 'android' tool with the 'update' action.
@ -24,17 +26,24 @@
application and should be checked into Version Control Systems. application and should be checked into Version Control Systems.
--> -->
<property file="build.properties" /> <property file="ant.properties" />
<!-- The default.properties file is created and updated by the 'android' <!-- The project.properties file is created and updated by the 'android'
tool, as well as ADT. tool, as well as ADT.
This contains project specific properties such as project target, and library
dependencies. Lower level build properties are stored in ant.properties
(or in .classpath for Eclipse projects).
This file is an integral part of the build system for your This file is an integral part of the build system for your
application and should be checked into Version Control Systems. --> application and should be checked into Version Control Systems. -->
<property file="default.properties" /> <loadproperties srcFile="project.properties" />
<!-- quick check on sdk.dir -->
<!-- Required pre-setup import --> <fail
<import file="${sdk.dir}/tools/ant/pre_setup.xml" /> message="sdk.dir is missing. Make sure to generate local.properties using 'android update project' or to inject it through an env var"
unless="sdk.dir"
/>
<!-- extension targets. Uncomment the ones where you want to do custom work <!-- extension targets. Uncomment the ones where you want to do custom work
@ -45,35 +54,32 @@
<target name="-pre-compile"> <target name="-pre-compile">
</target> </target>
[This is typically used for code obfuscation. /* This is typically used for code obfuscation.
Compiled code location: ${out.classes.absolute.dir} Compiled code location: ${out.classes.absolute.dir}
If this is not done in place, override ${out.dex.input.absolute.dir}] If this is not done in place, override ${out.dex.input.absolute.dir} */
<target name="-post-compile"> <target name="-post-compile">
</target> </target>
--> -->
<!-- Execute the Android Setup task that will setup some properties <!-- Import the actual build file.
specific to the target, and import the build rules files.
The rules file is imported from
<SDK>/tools/ant/
Depending on the project type it can be either:
- main_rules.xml
- lib_rules.xml
- test_rules.xml
To customize existing targets, there are two options: To customize existing targets, there are two options:
- Customize only one target: - Customize only one target:
- copy/paste the target into this file, *before* the - copy/paste the target into this file, *before* the
<setup> task. <import> task.
- customize it to your needs. - customize it to your needs.
- Customize the whole script. - Customize the whole content of build.xml
- copy/paste the content of the rules files (minus the top node) - copy/paste the content of the rules files (minus the top node)
into this file, *after* the <setup> task into this file, replacing the <import> task.
- disable the import of the rules by changing the setup task
below to <setup import="false" />.
- customize to your needs. - customize to your needs.
***********************
****** IMPORTANT ******
***********************
In all cases you must update the value of version-tag below to read 'custom' instead of an integer,
in order to avoid having your file be overridden by tools such as "android update project"
--> -->
<setup /> <!-- version-tag: 1 -->
<import file="${sdk.dir}/tools/ant/build.xml" />
</project> </project>

View File

@ -1,13 +0,0 @@
# This file is automatically generated by Android Tools.
# Do not modify this file -- YOUR CHANGES WILL BE ERASED!
#
# This file must be checked in Version Control Systems.
#
# To customize properties used by the Ant build system use,
# "build.properties", and override values to adapt the script to your
# project structure.
# Project target.
target=android-4
# Indicates whether an apk should be generated for each density.
split.density=false

13
project.properties Normal file
View File

@ -0,0 +1,13 @@
# This file is automatically generated by Android Tools.
# Do not modify this file -- YOUR CHANGES WILL BE ERASED!
#
# This file must be checked in Version Control Systems.
#
# To customize properties used by the Ant build system use,
# "ant.properties", and override values to adapt the script to your
# project structure.
# Indicates whether an apk should be generated for each density.
split.density=false
# Project target.
target=android-4

0
res/layout/main.xml → res/layout/log_view.xml Executable file → Normal file
View File

View File

@ -45,6 +45,13 @@
android:summaryOn="Incoming SMS will be stored in Messaging inbox" android:summaryOn="Incoming SMS will be stored in Messaging inbox"
></CheckBoxPreference> ></CheckBoxPreference>
<CheckBoxPreference
android:key="call_notifications"
android:title="Call notifications"
android:summaryOff="EnvayaSMS will not notify server when phone receives an incoming call"
android:summaryOn="EnvayaSMS will notify server when phone receives an incoming call"
></CheckBoxPreference>
<ListPreference <ListPreference
android:key="wifi_sleep_policy" android:key="wifi_sleep_policy"
android:title="Wi-Fi sleep policy" android:title="Wi-Fi sleep policy"

View File

@ -17,14 +17,20 @@ class EnvayaSMS
const STATUS_QUEUED = 'queued'; const STATUS_QUEUED = 'queued';
const STATUS_FAILED = 'failed'; const STATUS_FAILED = 'failed';
const STATUS_SENT = 'sent'; const STATUS_SENT = 'sent';
const STATUS_CANCELLED = 'cancelled';
const DEVICE_STATUS_POWER_CONNECTED = "power_connected"; const DEVICE_STATUS_POWER_CONNECTED = "power_connected";
const DEVICE_STATUS_POWER_DISCONNECTED = "power_disconnected"; const DEVICE_STATUS_POWER_DISCONNECTED = "power_disconnected";
const DEVICE_STATUS_BATTERY_LOW = "battery_low"; const DEVICE_STATUS_BATTERY_LOW = "battery_low";
const DEVICE_STATUS_BATTERY_OKAY = "battery_okay"; const DEVICE_STATUS_BATTERY_OKAY = "battery_okay";
const DEVICE_STATUS_SEND_LIMIT_EXCEEDED = "send_limit_exceeded";
const MESSAGE_TYPE_SMS = 'sms'; const MESSAGE_TYPE_SMS = 'sms';
const MESSAGE_TYPE_MMS = 'mms'; const MESSAGE_TYPE_MMS = 'mms';
const MESSAGE_TYPE_CALL = 'call';
const NETWORK_MOBILE = "MOBILE";
const NETWORK_WIFI = "WIFI";
static function escape($val) static function escape($val)
{ {
@ -47,6 +53,26 @@ class EnvayaSMS
} }
return static::$request; return static::$request;
} }
static function get_error_xml($message)
{
ob_start();
echo "<?xml version='1.0' encoding='UTF-8'?>\n";
echo "<response>";
echo "<error>";
echo EnvayaSMS::escape($message);
echo "</error>";
echo "</response>";
return ob_get_clean();
}
static function get_success_xml()
{
ob_start();
echo "<?xml version='1.0' encoding='UTF-8'?>\n";
echo "<response></response>";
return ob_get_clean();
}
} }
class EnvayaSMS_Request class EnvayaSMS_Request
@ -57,11 +83,27 @@ class EnvayaSMS_Request
public $phone_number; public $phone_number;
public $log; public $log;
public $version_name;
public $sdk_int;
public $manufacturer;
public $model;
public $network;
function __construct() function __construct()
{ {
$this->version = $_POST['version']; $this->version = $_POST['version'];
$this->phone_number = $_POST['phone_number']; $this->phone_number = $_POST['phone_number'];
$this->log = @$_POST['log']; $this->log = $_POST['log'];
$this->network = @$_POST['network'];
if (preg_match('#/(?P<version_name>[\w\.\-]+) \(Android; SDK (?P<sdk_int>\d+); (?P<manufacturer>[^;]*); (?P<model>[^\)]*)\)#',
@$_SERVER['HTTP_USER_AGENT'], $matches))
{
$this->version_name = $matches['version_name'];
$this->sdk_int = $matches['sdk_int'];
$this->manufacturer = $matches['manufacturer'];
$this->model = $matches['model'];
}
} }
function get_action() function get_action()
@ -130,15 +172,18 @@ class EnvayaSMS_Request
{ {
ob_start(); ob_start();
echo "<?xml version='1.0' encoding='UTF-8'?>\n"; echo "<?xml version='1.0' encoding='UTF-8'?>\n";
echo "<response>";
echo "<messages>"; echo "<messages>";
foreach ($messages as $message) foreach ($messages as $message)
{ {
$type = isset($message->type) ? $message->type : EnvayaSMS::MESSAGE_TYPE_SMS;
$id = isset($message->id) ? " id=\"".EnvayaSMS::escape($message->id)."\"" : ""; $id = isset($message->id) ? " id=\"".EnvayaSMS::escape($message->id)."\"" : "";
$to = isset($message->to) ? " to=\"".EnvayaSMS::escape($message->to)."\"" : ""; $to = isset($message->to) ? " to=\"".EnvayaSMS::escape($message->to)."\"" : "";
$priority = isset($message->priority) ? " priority=\"".$message->priority."\"" : ""; $priority = isset($message->priority) ? " priority=\"".$message->priority."\"" : "";
echo "<sms$id$to$priority>".EnvayaSMS::escape($message->message)."</sms>"; echo "<$type$id$to$priority>".EnvayaSMS::escape($message->message)."</$type>";
} }
echo "</messages>"; echo "</messages>";
echo "</response>";
return ob_get_clean(); return ob_get_clean();
} }
} }
@ -149,6 +194,7 @@ class EnvayaSMS_OutgoingMessage
public $to; // destination phone number public $to; // destination phone number
public $message; // content of SMS message public $message; // content of SMS message
public $priority; // integer priority, higher numbers will be sent first public $priority; // integer priority, higher numbers will be sent first
public $type; // EnvayaSMS::MESSAGE_TYPE_* value (default sms)
} }
class EnvayaSMS_Action class EnvayaSMS_Action
@ -194,15 +240,17 @@ class EnvayaSMS_Action_Incoming extends EnvayaSMS_Action
public $message_type; // EnvayaSMS::MESSAGE_TYPE_MMS or EnvayaSMS::MESSAGE_TYPE_SMS public $message_type; // EnvayaSMS::MESSAGE_TYPE_MMS or EnvayaSMS::MESSAGE_TYPE_SMS
public $mms_parts; // array of EnvayaSMS_MMS_Part instances public $mms_parts; // array of EnvayaSMS_MMS_Part instances
public $timestamp; // timestamp of incoming message (added in version 12) public $timestamp; // timestamp of incoming message (added in version 12)
public $age; // delay in ms between time when message originally received and when forwarded to server (added in version 18)
function __construct($request) function __construct($request)
{ {
parent::__construct($request); parent::__construct($request);
$this->type = EnvayaSMS::ACTION_INCOMING; $this->type = EnvayaSMS::ACTION_INCOMING;
$this->from = $_POST['from']; $this->from = $_POST['from'];
$this->message = $_POST['message']; $this->message = @$_POST['message'];
$this->message_type = $_POST['message_type']; $this->message_type = $_POST['message_type'];
$this->timestamp = @$_POST['timestamp']; $this->timestamp = @$_POST['timestamp'];
$this->age = @$_POST['age'];
if ($this->message_type == EnvayaSMS::MESSAGE_TYPE_MMS) if ($this->message_type == EnvayaSMS::MESSAGE_TYPE_MMS)
{ {
@ -247,7 +295,7 @@ class EnvayaSMS_Action_SendStatus extends EnvayaSMS_Action
{ {
public $status; // EnvayaSMS::STATUS_* values public $status; // EnvayaSMS::STATUS_* values
public $id; // server ID previously used in EnvayaSMS_OutgoingMessage public $id; // server ID previously used in EnvayaSMS_OutgoingMessage
public $error; // textual descrption of error (if applicable) public $error; // textual description of error (if applicable)
function __construct($request) function __construct($request)
{ {

View File

@ -0,0 +1,2 @@
*
!.gitignore

View File

@ -14,11 +14,13 @@ $phone_number = $request->phone_number;
$password = @$PASSWORDS[$phone_number]; $password = @$PASSWORDS[$phone_number];
header("Content-Type: text/xml");
if (!isset($password) || !$request->is_validated($password)) if (!isset($password) || !$request->is_validated($password))
{ {
header("HTTP/1.1 403 Forbidden"); header("HTTP/1.1 403 Forbidden");
error_log("Invalid request signature"); error_log("Invalid request signature");
echo "Invalid request signature"; echo EnvayaSMS::get_error_xml("Invalid request signature");
return; return;
} }
@ -37,14 +39,30 @@ $action = $request->get_action();
switch ($action->type) switch ($action->type)
{ {
case EnvayaSMS::ACTION_INCOMING: case EnvayaSMS::ACTION_INCOMING:
error_log("Received SMS from {$action->from}"); $type = strtoupper($action->message_type);
error_log("Received $type from {$action->from}");
error_log(" message: {$action->message}");
if ($action->message_type == EnvayaSMS::MESSAGE_TYPE_MMS)
{
foreach ($action->mms_parts as $mms_part)
{
$ext_map = array('image/jpeg' => 'jpg', 'image/gif' => 'gif', 'text/plain' => 'txt', 'application/smil' => 'smil');
$ext = @$ext_map[$mms_part->type] ?: "unk";
$filename = "mms_parts/" . uniqid('mms') . ".$ext";
copy($mms_part->tmp_name, dirname(__DIR__)."/$filename");
error_log(" mms part type {$mms_part->type} saved to {$filename}");
}
}
$reply = new EnvayaSMS_OutgoingMessage(); $reply = new EnvayaSMS_OutgoingMessage();
$reply->message = "You said: {$action->message}"; $reply->message = "You said: {$action->message}";
error_log("Sending reply: {$reply->message}"); error_log("Sending reply: {$reply->message}");
header("Content-Type: text/xml");
echo $action->get_response_xml(array($reply)); echo $action->get_response_xml(array($reply));
return; return;
@ -70,7 +88,6 @@ switch ($action->type)
} }
closedir($dir); closedir($dir);
header("Content-Type: text/xml");
echo $action->get_response_xml($messages); echo $action->get_response_xml($messages);
return; return;
@ -81,23 +98,23 @@ switch ($action->type)
// delete file with matching id // delete file with matching id
if (preg_match('#^\w+$#', $id) && unlink("$OUTGOING_DIR_NAME/$id.json")) if (preg_match('#^\w+$#', $id) && unlink("$OUTGOING_DIR_NAME/$id.json"))
{ {
echo "OK"; echo EnvayaSMS::get_success_xml();
} }
else else
{ {
header("HTTP/1.1 404 Not Found"); header("HTTP/1.1 404 Not Found");
echo "invalid id"; echo EnvayaSMS::get_error_xml("Invalid id");
} }
return; return;
case EnvayaSMS::ACTION_DEVICE_STATUS: case EnvayaSMS::ACTION_DEVICE_STATUS:
error_log("device_status = {$action->status}"); error_log("device_status = {$action->status}");
echo "OK"; echo EnvayaSMS::get_success_xml();
return; return;
case EnvayaSMS::ACTION_TEST: case EnvayaSMS::ACTION_TEST:
echo "OK"; echo EnvayaSMS::get_success_xml();
return; return;
default: default:
header("HTTP/1.1 404 Not Found"); header("HTTP/1.1 404 Not Found");
echo "Invalid action"; echo EnvayaSMS::get_error_xml("Invalid action");
return; return;
} }

View File

@ -14,14 +14,15 @@ import android.net.ConnectivityManager;
import android.net.NetworkInfo; import android.net.NetworkInfo;
import android.net.Uri; import android.net.Uri;
import android.net.wifi.WifiManager; import android.net.wifi.WifiManager;
import android.os.AsyncTask;
import android.os.Bundle; import android.os.Bundle;
import android.os.SystemClock; import android.os.SystemClock;
import android.preference.PreferenceManager; import android.preference.PreferenceManager;
import android.telephony.PhoneStateListener;
import android.telephony.TelephonyManager;
import android.text.Html; import android.text.Html;
import android.text.SpannableStringBuilder; import android.text.SpannableStringBuilder;
import android.util.Log; import android.util.Log;
import java.io.IOException;
import java.net.InetAddress;
import java.text.DateFormat; import java.text.DateFormat;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Date; import java.util.Date;
@ -42,9 +43,10 @@ import org.apache.http.params.HttpConnectionParams;
import org.apache.http.params.HttpParams; import org.apache.http.params.HttpParams;
import org.apache.http.params.HttpProtocolParams; import org.apache.http.params.HttpProtocolParams;
import org.envaya.sms.receiver.OutgoingMessagePoller; import org.envaya.sms.receiver.OutgoingMessagePoller;
import org.envaya.sms.receiver.ReenableWifiReceiver; import org.envaya.sms.task.CheckConnectivityTask;
import org.envaya.sms.task.HttpTask; import org.envaya.sms.task.HttpTask;
import org.envaya.sms.task.PollerTask; import org.envaya.sms.task.PollerTask;
import org.apache.http.message.BasicNameValuePair;
import org.json.JSONArray; import org.json.JSONArray;
import org.json.JSONException; import org.json.JSONException;
@ -59,14 +61,17 @@ public final class App extends Application {
public static final String STATUS_QUEUED = "queued"; public static final String STATUS_QUEUED = "queued";
public static final String STATUS_FAILED = "failed"; public static final String STATUS_FAILED = "failed";
public static final String STATUS_SENT = "sent"; public static final String STATUS_SENT = "sent";
public static final String STATUS_CANCELLED = "cancelled";
public static final String DEVICE_STATUS_POWER_CONNECTED = "power_connected"; public static final String DEVICE_STATUS_POWER_CONNECTED = "power_connected";
public static final String DEVICE_STATUS_POWER_DISCONNECTED = "power_disconnected"; public static final String DEVICE_STATUS_POWER_DISCONNECTED = "power_disconnected";
public static final String DEVICE_STATUS_BATTERY_LOW = "battery_low"; public static final String DEVICE_STATUS_BATTERY_LOW = "battery_low";
public static final String DEVICE_STATUS_BATTERY_OKAY = "battery_okay"; public static final String DEVICE_STATUS_BATTERY_OKAY = "battery_okay";
public static final String DEVICE_STATUS_SEND_LIMIT_EXCEEDED = "send_limit_exceeded";
public static final String MESSAGE_TYPE_MMS = "mms"; public static final String MESSAGE_TYPE_MMS = "mms";
public static final String MESSAGE_TYPE_SMS = "sms"; public static final String MESSAGE_TYPE_SMS = "sms";
public static final String MESSAGE_TYPE_CALL = "call";
public static final String LOG_NAME = "EnvayaSMS"; public static final String LOG_NAME = "EnvayaSMS";
@ -96,11 +101,13 @@ public final class App extends Application {
public static final String STATUS_EXTRA_NUM_PARTS = "num_parts"; public static final String STATUS_EXTRA_NUM_PARTS = "num_parts";
public static final int MAX_DISPLAYED_LOG = 8000; public static final int MAX_DISPLAYED_LOG = 8000;
public static final int LOG_TIMESTAMP_INTERVAL = 60000; // ms public static final int LOG_TIMESTAMP_INTERVAL = 30000; //60000; // ms
public static final int HTTP_CONNECTION_TIMEOUT = 10000; // ms public static final int HTTP_CONNECTION_TIMEOUT = 10000; // ms
public static final int HTTP_SOCKET_TIMEOUT = 60000; // ms public static final int HTTP_SOCKET_TIMEOUT = 60000; // ms
public static final int MESSAGE_SEND_TIMEOUT = 30000; // ms
// Each QueuedMessage is identified within our internal Map by its Uri. // Each QueuedMessage is identified within our internal Map by its Uri.
// Currently QueuedMessage instances are only available within EnvayaSMS, // Currently QueuedMessage instances are only available within EnvayaSMS,
// (but they could be made available to other applications later via a ContentProvider) // (but they could be made available to other applications later via a ContentProvider)
@ -113,7 +120,7 @@ public final class App extends Application {
public static final int DISABLE_WIFI_INTERVAL = 3600000; public static final int DISABLE_WIFI_INTERVAL = 3600000;
// how often we can automatically failover between wifi/mobile connection // how often we can automatically failover between wifi/mobile connection
public static final int CONNECTIVITY_FAILOVER_INTERVAL = 1800000; public static final int CONNECTIVITY_FAILOVER_INTERVAL = 900000;
// max per-app outgoing SMS rate used by com.android.internal.telephony.SMSDispatcher // max per-app outgoing SMS rate used by com.android.internal.telephony.SMSDispatcher
// with a slightly longer check period to account for variance in the time difference // with a slightly longer check period to account for variance in the time difference
@ -145,6 +152,9 @@ public final class App extends Application {
private int outgoingMessageCount = -1; private int outgoingMessageCount = -1;
private MmsUtils mmsUtils; private MmsUtils mmsUtils;
private CallListener callListener;
private boolean connectivityError = false;
@Override @Override
public void onCreate() public void onCreate()
@ -154,6 +164,8 @@ public final class App extends Application {
settings = PreferenceManager.getDefaultSharedPreferences(this); settings = PreferenceManager.getDefaultSharedPreferences(this);
mmsUtils = new MmsUtils(this); mmsUtils = new MmsUtils(this);
callListener = new CallListener(this);
outgoingMessagePackages.add(getPackageName()); outgoingMessagePackages.add(getPackageName());
mmsObserver = new MmsObserver(this); mmsObserver = new MmsObserver(this);
@ -170,16 +182,57 @@ public final class App extends Application {
} }
updateExpansionPacks(); updateExpansionPacks();
configuredChanged();
}
public void configuredChanged()
{
log(Html.fromHtml( log(Html.fromHtml(
isEnabled() ? "<b>SMS gateway running.</b>" : "<b>SMS gateway disabled.</b>")); isEnabled() ? "<b>SMS gateway running ("+getDisplayString(getPhoneNumber())+").</b>"
: "<b>SMS gateway disabled.</b>"));
log("Server URL is: " + getDisplayString(getServerUrl())); log("Server URL: " + getDisplayString(getServerUrl()));
log("Your phone number is: " + getDisplayString(getPhoneNumber())); log("Keep new messages: " + (getKeepInInbox() ? "YES": "NO"));
log("Call notifications: " + (callNotificationsEnabled() ? "ON": "OFF"));
log("Network failover: " + (isNetworkFailoverEnabled() ? "ON": "OFF"));
boolean ignoreShortcodes = ignoreShortcodes();
boolean ignoreNonNumeric = ignoreNonNumeric();
List<String> ignoredNumbers = getIgnoredPhoneNumbers();
if (ignoredNumbers.size() > 0 || ignoreShortcodes || ignoreNonNumeric)
{
log("Ignored phone numbers:");
if (ignoreShortcodes || ignoreNonNumeric)
{
String ignoreDesc = " ";
if (ignoreShortcodes)
{
ignoreDesc += "all shortcodes";
}
if (ignoreShortcodes && ignoreNonNumeric)
{
ignoreDesc += ", ";
}
if (ignoreNonNumeric)
{
ignoreDesc += "all non-numeric";
}
log(ignoreDesc);
}
for (String sender : ignoredNumbers)
{
log(" " + sender);
}
}
if (isTestMode()) if (isTestMode())
{ {
log("Test mode is ON"); log("Test mode: ON");
log("Test phone numbers:"); log("Test phone numbers:");
for (String sender : getTestPhoneNumbers()) for (String sender : getTestPhoneNumbers())
@ -188,20 +241,27 @@ public final class App extends Application {
} }
} }
enabledChanged(); log(Html.fromHtml("<b>To change these settings, click Menu, then Settings.</b>"));
log(Html.fromHtml("<b>Press Menu to edit settings.</b>")); enabledChanged();
} }
public void enabledChanged() public void enabledChanged()
{ {
TelephonyManager telephony = (TelephonyManager)
getSystemService(Context.TELEPHONY_SERVICE);
if (isEnabled()) if (isEnabled())
{ {
mmsObserver.register(); mmsObserver.register();
telephony.listen(callListener, PhoneStateListener.LISTEN_CALL_STATE);
} }
else else
{ {
mmsObserver.unregister(); mmsObserver.unregister();
telephony.listen(callListener, PhoneStateListener.LISTEN_NONE);
} }
setOutgoingMessageAlarm(); setOutgoingMessageAlarm();
@ -259,6 +319,13 @@ public final class App extends Application {
+ getOutgoingMessageLimit() + " in 1 hour reached"); + getOutgoingMessageLimit() + " in 1 hour reached");
log("To increase this limit, install an expansion pack."); log("To increase this limit, install an expansion pack.");
HttpTask task = new HttpTask(this,
new BasicNameValuePair("action", App.ACTION_DEVICE_STATUS),
new BasicNameValuePair("status", App.DEVICE_STATUS_SEND_LIMIT_EXCEEDED)
);
task.setRetryOnConnectivityError(true);
task.execute();
return null; return null;
} }
@ -338,13 +405,14 @@ public final class App extends Application {
sendOrderedBroadcast( sendOrderedBroadcast(
new Intent(App.QUERY_EXPANSION_PACKS_INTENT), new Intent(App.QUERY_EXPANSION_PACKS_INTENT),
"android.permission.SEND_SMS", null,
new BroadcastReceiver() { new BroadcastReceiver() {
@Override @Override
public void onReceive(Context context, Intent resultIntent) { public void onReceive(Context context, Intent resultIntent) {
setExpansionPacks(this.getResultExtras(false) Bundle extras = this.getResultExtras(false);
.getStringArrayList(App.QUERY_EXPANSION_PACKS_EXTRA_PACKAGES));
setExpansionPacks(extras.getStringArrayList(App.QUERY_EXPANSION_PACKS_EXTRA_PACKAGES));
} }
}, },
@ -423,6 +491,11 @@ public final class App extends Application {
} }
} }
public boolean callNotificationsEnabled()
{
return tryGetBooleanSetting("call_notifications", false);
}
public String getServerUrl() { public String getServerUrl() {
return settings.getString("server_url", ""); return settings.getString("server_url", "");
} }
@ -437,32 +510,44 @@ public final class App extends Application {
public boolean isEnabled() public boolean isEnabled()
{ {
return settings.getBoolean("enabled", false); return tryGetBooleanSetting("enabled", false);
}
public boolean tryGetBooleanSetting(String name, boolean defaultValue)
{
try
{
return settings.getBoolean(name, defaultValue);
}
catch (Exception ex)
{
return defaultValue;
}
} }
public boolean isNetworkFailoverEnabled() public boolean isNetworkFailoverEnabled()
{ {
return settings.getBoolean("network_failover", false); return tryGetBooleanSetting("network_failover", false);
} }
public boolean isTestMode() public boolean isTestMode()
{ {
return settings.getBoolean("test_mode", false); return tryGetBooleanSetting("test_mode", false);
} }
public boolean getKeepInInbox() public boolean getKeepInInbox()
{ {
return settings.getBoolean("keep_in_inbox", false); return tryGetBooleanSetting("keep_in_inbox", false);
} }
public boolean ignoreShortcodes() public boolean ignoreShortcodes()
{ {
return settings.getBoolean("ignore_shortcodes", true); return tryGetBooleanSetting("ignore_shortcodes", true);
} }
public boolean ignoreNonNumeric() public boolean ignoreNonNumeric()
{ {
return settings.getBoolean("ignore_non_numeric", true); return tryGetBooleanSetting("ignore_non_numeric", true);
} }
public String getPassword() { public String getPassword() {
@ -644,6 +729,11 @@ public final class App extends Application {
).commit(); ).commit();
} }
public synchronized void saveStringSetting(String key, String value)
{
settings.edit().putString(key, value).commit();
}
public synchronized void saveBooleanSetting(String key, boolean value) public synchronized void saveBooleanSetting(String key, boolean value)
{ {
settings.edit().putBoolean(key, value).commit(); settings.edit().putBoolean(key, value).commit();
@ -761,20 +851,20 @@ public final class App extends Application {
public synchronized boolean canCheck() public synchronized boolean canCheck()
{ {
long time = SystemClock.elapsedRealtime(); long time = System.currentTimeMillis();
return (time - lastCheckTime >= App.CONNECTIVITY_FAILOVER_INTERVAL); return (time - lastCheckTime >= App.CONNECTIVITY_FAILOVER_INTERVAL);
} }
public void setChecked() public void setChecked()
{ {
lastCheckTime = SystemClock.elapsedRealtime(); lastCheckTime = System.currentTimeMillis();
} }
} }
private Map<Integer,ConnectivityCheckState> connectivityCheckStates private Map<Integer,ConnectivityCheckState> connectivityCheckStates
= new HashMap<Integer, ConnectivityCheckState>(); = new HashMap<Integer, ConnectivityCheckState>();
private Thread connectivityThread; private CheckConnectivityTask checkConnectivityTask;
/* /*
* Normally we rely on Android to automatically switch between * Normally we rely on Android to automatically switch between
@ -832,79 +922,20 @@ public final class App extends Application {
} }
if (!state.canCheck() if (!state.canCheck()
|| (connectivityThread != null && connectivityThread.isAlive())) || (checkConnectivityTask != null && checkConnectivityTask.getStatus() != AsyncTask.Status.FINISHED))
{ {
return; return;
} }
state.setChecked(); state.setChecked();
connectivityThread = new Thread() {
@Override
public void run()
{
Uri serverUrl = Uri.parse(getServerUrl()); Uri serverUrl = Uri.parse(getServerUrl());
String hostName = serverUrl.getHost(); String hostName = serverUrl.getHost();
log("Checking connectivity to "+hostName+"..."); log("Checking connectivity to "+hostName+"...");
try checkConnectivityTask = new CheckConnectivityTask(this, hostName, networkType);
{ checkConnectivityTask.execute();
InetAddress addr = InetAddress.getByName(hostName);
if (addr.isReachable(App.HTTP_CONNECTION_TIMEOUT))
{
log("OK");
onConnectivityRestored();
return;
}
}
catch (IOException ex)
{
// just what we suspected...
// server not reachable on this interface
}
log("Can't connect to "+hostName+".");
WifiManager wmgr = (WifiManager)getSystemService(Context.WIFI_SERVICE);
if (!isNetworkFailoverEnabled())
{
log("Network failover disabled.");
}
else if (networkType == ConnectivityManager.TYPE_WIFI)
{
log("Switching from WIFI to MOBILE");
PendingIntent pendingIntent = PendingIntent.getBroadcast(App.this,
0,
new Intent(App.this, ReenableWifiReceiver.class),
0);
// set an alarm to try restoring Wi-Fi in a little while
AlarmManager alarm =
(AlarmManager)getSystemService(Context.ALARM_SERVICE);
alarm.set(
AlarmManager.ELAPSED_REALTIME_WAKEUP,
SystemClock.elapsedRealtime() + App.DISABLE_WIFI_INTERVAL,
pendingIntent);
wmgr.setWifiEnabled(false);
}
else if (networkType == ConnectivityManager.TYPE_MOBILE
&& !wmgr.isWifiEnabled())
{
log("Switching from MOBILE to WIFI");
wmgr.setWifiEnabled(true);
}
else
{
log("Can't automatically fix connectivity.");
}
}
};
connectivityThread.start();
} }
private int activeNetworkType = -1; private int activeNetworkType = -1;
@ -933,8 +964,21 @@ public final class App extends Application {
asyncCheckConnectivity(); asyncCheckConnectivity();
} }
private void onConnectivityRestored() public boolean hasConnectivityError()
{ {
return connectivityError;
}
public synchronized void onConnectivityError()
{
connectivityError = true;
asyncCheckConnectivity();
}
public synchronized void onConnectivityRestored()
{
connectivityError = false;
inbox.retryAll(); inbox.retryAll();
if (getOutgoingPollSeconds() > 0) if (getOutgoingPollSeconds() > 0)

View File

@ -0,0 +1,31 @@
package org.envaya.sms;
import android.telephony.PhoneStateListener;
import android.telephony.TelephonyManager;
public class CallListener extends PhoneStateListener {
private App app;
public CallListener(App app)
{
this.app = app;
}
@Override
public void onCallStateChanged(int state,String incomingNumber) {
if (state == TelephonyManager.CALL_STATE_RINGING)
{
IncomingCall call = new IncomingCall(app, incomingNumber, System.currentTimeMillis());
if (call.isForwardable())
{
app.inbox.forwardMessage(call);
}
else
{
app.log("Ignoring incoming call from " + call.getFrom());
}
}
}
}

View File

@ -26,7 +26,7 @@ import android.util.Log;
import java.lang.reflect.InvocationTargetException; import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import org.envaya.sms.ui.Main; import org.envaya.sms.ui.LogView;
/* /*
* Service running in foreground to make sure App instance stays * Service running in foreground to make sure App instance stays
@ -159,7 +159,7 @@ public class ForegroundService extends Service {
System.currentTimeMillis()); System.currentTimeMillis());
PendingIntent contentIntent = PendingIntent.getActivity(this, 0, PendingIntent contentIntent = PendingIntent.getActivity(this, 0,
new Intent(this, Main.class), 0); new Intent(this, LogView.class), 0);
notification.setLatestEventInfo(this, notification.setLatestEventInfo(this,
"EnvayaSMS running", "EnvayaSMS running",

View File

@ -0,0 +1,49 @@
/*
* To change this template, choose Tools | Templates
* and open the template in the editor.
*/
package org.envaya.sms;
import android.net.Uri;
import org.envaya.sms.task.ForwarderTask;
public class IncomingCall extends IncomingMessage {
private long id;
private static long nextId = 1;
public IncomingCall(App app, String from, long timestampMillis)
{
super(app, from, timestampMillis);
this.id = getNextId();
}
public static synchronized long getNextId()
{
long id = nextId;
nextId++;
return id;
}
public String getDisplayType()
{
return "call";
}
public String getMessageType()
{
return App.MESSAGE_TYPE_CALL;
}
@Override
public boolean isForwardable()
{
return app.callNotificationsEnabled() && super.isForwardable();
}
public Uri getUri()
{
return Uri.withAppendedPath(App.INCOMING_URI, "call/" + id);
}
}

View File

@ -1,13 +1,19 @@
package org.envaya.sms; package org.envaya.sms;
import android.content.Intent; import android.content.Intent;
import android.os.SystemClock;
import org.envaya.sms.receiver.IncomingMessageRetry; import org.envaya.sms.receiver.IncomingMessageRetry;
import org.envaya.sms.task.ForwarderTask;
import org.apache.http.message.BasicNameValuePair;
public abstract class IncomingMessage extends QueuedMessage { public abstract class IncomingMessage extends QueuedMessage {
protected String from; protected String from;
protected String message = "";
protected long timestamp; // unix timestamp in milliseconds protected long timestamp; // unix timestamp in milliseconds
protected long timeReceived; // SystemClock.elapsedRealtime
private ProcessingState state = ProcessingState.None; private ProcessingState state = ProcessingState.None;
public enum ProcessingState public enum ProcessingState
@ -24,6 +30,18 @@ public abstract class IncomingMessage extends QueuedMessage {
super(app); super(app);
this.from = from; this.from = from;
this.timestamp = timestamp; this.timestamp = timestamp;
this.timeReceived = SystemClock.elapsedRealtime();
}
public String getMessageBody()
{
return message;
}
public long getAge()
{
return SystemClock.elapsedRealtime() - timeReceived;
} }
public long getTimestamp() public long getTimestamp()
@ -77,5 +95,27 @@ public abstract class IncomingMessage extends QueuedMessage {
return getDisplayType() + " from " + getFrom(); return getDisplayType() + " from " + getFrom();
} }
public abstract void tryForwardToServer(); public void tryForwardToServer()
{
if (numRetries > 0)
{
app.log("Retrying forwarding " + getDescription());
}
getForwarderTask().execute();
}
public abstract String getMessageType();
protected ForwarderTask getForwarderTask()
{
return new ForwarderTask(this,
new BasicNameValuePair("message_type", getMessageType()),
new BasicNameValuePair("message", getMessageBody()),
new BasicNameValuePair("action", App.ACTION_INCOMING),
new BasicNameValuePair("from", getFrom()),
new BasicNameValuePair("timestamp", "" + getTimestamp()),
new BasicNameValuePair("age", "" + getAge())
);
}
} }

View File

@ -11,7 +11,6 @@ import java.util.List;
import org.apache.http.entity.mime.FormBodyPart; import org.apache.http.entity.mime.FormBodyPart;
import org.apache.http.entity.mime.content.ByteArrayBody; import org.apache.http.entity.mime.content.ByteArrayBody;
import org.apache.http.entity.mime.content.ContentBody; import org.apache.http.entity.mime.content.ContentBody;
import org.apache.http.message.BasicNameValuePair;
import org.envaya.sms.task.ForwarderTask; import org.envaya.sms.task.ForwarderTask;
public class IncomingMms extends IncomingMessage { public class IncomingMms extends IncomingMessage {
@ -76,22 +75,13 @@ public class IncomingMms extends IncomingMessage {
return builder.toString(); return builder.toString();
} }
public void tryForwardToServer() @Override
protected ForwarderTask getForwarderTask()
{ {
if (numRetries > 0)
{
app.log("Retrying forwarding MMS from " + from);
}
else
{
app.log("Forwarding MMS to server...");
}
List<FormBodyPart> formParts = new ArrayList<FormBodyPart>(); List<FormBodyPart> formParts = new ArrayList<FormBodyPart>();
int i = 0; int i = 0;
String message = "";
JSONArray partsMetadata = new JSONArray(); JSONArray partsMetadata = new JSONArray();
for (MmsPart part : parts) for (MmsPart part : parts)
@ -101,11 +91,6 @@ public class IncomingMms extends IncomingMessage {
String contentType = part.getContentType(); String contentType = part.getContentType();
String partName = part.getName(); String partName = part.getName();
if ("text/plain".equals(contentType))
{
message = text;
}
ContentBody body; ContentBody body;
if (text != null) if (text != null)
@ -154,18 +139,33 @@ public class IncomingMms extends IncomingMessage {
i++; i++;
} }
ForwarderTask task = new ForwarderTask(this, ForwarderTask task = super.getForwarderTask();
new BasicNameValuePair("message", message), task.addParam("mms_parts", partsMetadata.toString());
new BasicNameValuePair("message_type", App.MESSAGE_TYPE_MMS),
new BasicNameValuePair("mms_parts", partsMetadata.toString())
);
task.setFormParts(formParts); task.setFormParts(formParts);
task.execute(); return task;
}
@Override
public String getMessageBody()
{
for (MmsPart part : parts)
{
if ("text/plain".equals(part.getContentType()))
{
return part.getText();
}
}
return "";
} }
public Uri getUri() public Uri getUri()
{ {
return Uri.withAppendedPath(App.INCOMING_URI, "mms/" + id); return Uri.withAppendedPath(App.INCOMING_URI, "mms/" + id);
} }
public String getMessageType()
{
return App.MESSAGE_TYPE_MMS;
}
} }

View File

@ -5,14 +5,11 @@ import android.net.Uri;
import android.telephony.SmsMessage; import android.telephony.SmsMessage;
import java.security.InvalidParameterException; import java.security.InvalidParameterException;
import java.util.List; import java.util.List;
import org.apache.http.message.BasicNameValuePair;
import org.envaya.sms.task.ForwarderTask; import org.envaya.sms.task.ForwarderTask;
public class IncomingSms extends IncomingMessage { public class IncomingSms extends IncomingMessage {
protected String message;
// constructor for SMS retrieved from android.provider.Telephony.SMS_RECEIVED intent // constructor for SMS retrieved from android.provider.Telephony.SMS_RECEIVED intent
public IncomingSms(App app, List<SmsMessage> smsParts) throws InvalidParameterException { public IncomingSms(App app, List<SmsMessage> smsParts) throws InvalidParameterException {
super(app, super(app,
@ -44,11 +41,6 @@ public class IncomingSms extends IncomingMessage {
this.message = message; this.message = message;
} }
public String getMessageBody()
{
return message;
}
public String getDisplayType() public String getDisplayType()
{ {
return "SMS"; return "SMS";
@ -63,17 +55,8 @@ public class IncomingSms extends IncomingMessage {
Uri.encode(message)); Uri.encode(message));
} }
public void tryForwardToServer() { public String getMessageType()
if (numRetries > 0)
{ {
app.log("Retrying forwarding SMS from " + from); return App.MESSAGE_TYPE_SMS;
} }
new ForwarderTask(this,
new BasicNameValuePair("message_type", App.MESSAGE_TYPE_SMS),
new BasicNameValuePair("message", getMessageBody())
).execute();
}
} }

View File

@ -4,11 +4,7 @@ package org.envaya.sms;
import android.content.ContentResolver; import android.content.ContentResolver;
import android.database.Cursor; import android.database.Cursor;
import android.net.Uri; import android.net.Uri;
import java.io.File; import java.util.*;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/* /*
* Utilities for parsing IncomingMms from the MMS content provider tables, * Utilities for parsing IncomingMms from the MMS content provider tables,
@ -56,9 +52,25 @@ public class MmsUtils
{ {
long partId = cur.getLong(0); long partId = cur.getLong(0);
String contentType = cur.getString(1);
if (contentType == null)
{
continue;
}
MmsPart part = new MmsPart(app, partId); MmsPart part = new MmsPart(app, partId);
part.setContentType(cur.getString(1));
part.setName(cur.getString(2)); part.setContentType(contentType);
String name = cur.getString(2);
if (name == null || name.length() == 0)
{
name = UUID.randomUUID().toString();
}
part.setName(name);
part.setDataFile(cur.getString(5)); part.setDataFile(cur.getString(5));
@ -117,8 +129,16 @@ public class MmsUtils
long id = c.getLong(0); long id = c.getLong(0);
long date = c.getLong(2); long date = c.getLong(2);
String from = getSenderNumber(id);
if (from == null)
{
app.log("Ignoring MMS "+id+" for now because sender number is null");
continue;
}
IncomingMms mms = new IncomingMms(app, IncomingMms mms = new IncomingMms(app,
getSenderNumber(id), from,
date * 1000, // MMS timestamp is in seconds for some reason, date * 1000, // MMS timestamp is in seconds for some reason,
// while everything else is in ms // while everything else is in ms
id); id);

View File

@ -6,8 +6,7 @@ import android.app.PendingIntent;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.net.Uri; import android.net.Uri;
import android.telephony.SmsManager; import android.os.SystemClock;
import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.Comparator; import java.util.Comparator;
import java.util.HashMap; import java.util.HashMap;
@ -19,6 +18,7 @@ import java.util.Queue;
import java.util.Set; import java.util.Set;
import org.apache.http.message.BasicNameValuePair; import org.apache.http.message.BasicNameValuePair;
import org.envaya.sms.receiver.DequeueOutgoingMessageReceiver; import org.envaya.sms.receiver.DequeueOutgoingMessageReceiver;
import org.envaya.sms.receiver.OutgoingMessagePoller;
import org.envaya.sms.task.HttpTask; import org.envaya.sms.task.HttpTask;
public class Outbox { public class Outbox {
@ -75,7 +75,10 @@ public class Outbox {
logMessage = "sent successfully"; logMessage = "sent successfully";
} else if (status.equals(App.STATUS_FAILED)) { } else if (status.equals(App.STATUS_FAILED)) {
logMessage = "could not be sent (" + errorMessage + ")"; logMessage = "could not be sent (" + errorMessage + ")";
} else { } else if (status.equals(App.STATUS_CANCELLED)) {
logMessage = "cancelled";
}
else {
logMessage = "queued"; logMessage = "queued";
} }
String smsDesc = sms.getLogName(); String smsDesc = sms.getLogName();
@ -116,6 +119,8 @@ public class Outbox {
{ {
sms.setProcessingState(OutgoingMessage.ProcessingState.Sent); sms.setProcessingState(OutgoingMessage.ProcessingState.Sent);
sms.clearSendTimeout();
notifyMessageStatus(sms, App.STATUS_SENT, ""); notifyMessageStatus(sms, App.STATUS_SENT, "");
Uri uri = sms.getUri(); Uri uri = sms.getUri();
@ -149,6 +154,8 @@ public class Outbox {
public synchronized void messageFailed(OutgoingMessage sms, String error) public synchronized void messageFailed(OutgoingMessage sms, String error)
{ {
sms.clearSendTimeout();
if (sms.scheduleRetry()) if (sms.scheduleRetry())
{ {
sms.setProcessingState(OutgoingMessage.ProcessingState.Scheduled); sms.setProcessingState(OutgoingMessage.ProcessingState.Scheduled);
@ -164,49 +171,33 @@ public class Outbox {
maybeDequeueMessage(); maybeDequeueMessage();
} }
public synchronized void sendMessage(OutgoingMessage sms) { public synchronized void sendMessage(OutgoingMessage message) {
String to = sms.getTo(); try
if (to == null || to.length() == 0)
{ {
notifyMessageStatus(sms, App.STATUS_FAILED, message.validate();
"Destination address is empty"); }
catch (ValidationException ex)
{
notifyMessageStatus(message, App.STATUS_FAILED, ex.getMessage());
return; return;
} }
if (!app.isForwardablePhoneNumber(to)) Uri uri = message.getUri();
{
// this is mostly to prevent accidentally sending real messages to
// random people while testing...
notifyMessageStatus(sms, App.STATUS_FAILED,
"Destination address is not allowed");
return;
}
String messageBody = sms.getMessageBody();
if (messageBody == null || messageBody.length() == 0)
{
notifyMessageStatus(sms, App.STATUS_FAILED,
"Message body is empty");
return;
}
Uri uri = sms.getUri();
if (outgoingMessages.containsKey(uri)) { if (outgoingMessages.containsKey(uri)) {
app.debug("Duplicate outgoing " + sms.getLogName() + ", skipping"); app.debug("Duplicate outgoing " + message.getLogName() + ", skipping");
return; return;
} }
if (recentSentMessageUris.contains(uri)) if (recentSentMessageUris.contains(uri))
{ {
app.debug("Outgoing " + sms.getLogName() + " already sent, re-notifying server"); app.debug("Outgoing " + message.getLogName() + " already sent, re-notifying server");
notifyMessageStatus(sms, App.STATUS_SENT, ""); notifyMessageStatus(message, App.STATUS_SENT, "");
return; return;
} }
outgoingMessages.put(uri, sms); outgoingMessages.put(uri, message);
enqueueMessage(sms); enqueueMessage(message);
} }
public synchronized void deleteMessage(OutgoingMessage message) public synchronized void deleteMessage(OutgoingMessage message)
@ -222,7 +213,7 @@ public class Outbox {
numSendingOutgoingMessages--; numSendingOutgoingMessages--;
} }
notifyMessageStatus(message, App.STATUS_FAILED, notifyMessageStatus(message, App.STATUS_CANCELLED,
"deleted by user"); "deleted by user");
app.log(message.getDescription() + " deleted"); app.log(message.getDescription() + " deleted");
notifyChanged(); notifyChanged();
@ -233,32 +224,18 @@ public class Outbox {
long now = System.currentTimeMillis(); long now = System.currentTimeMillis();
if (nextValidOutgoingTime <= now && numSendingOutgoingMessages < 2) if (nextValidOutgoingTime <= now && numSendingOutgoingMessages < 2)
{ {
OutgoingMessage sms = outgoingQueue.peek(); OutgoingMessage message = outgoingQueue.peek();
if (sms == null) if (message == null)
{ {
return; return;
} }
SmsManager smgr = SmsManager.getDefault(); OutgoingMessage.ScheduleInfo schedule = message.scheduleSend();
ArrayList<String> bodyParts = smgr.divideMessage(sms.getMessageBody());
int numParts = bodyParts.size(); if (!schedule.now)
if (numParts > App.OUTGOING_SMS_MAX_COUNT)
{ {
outgoingQueue.poll(); nextValidOutgoingTime = schedule.time;
outgoingMessages.remove(sms.getUri());
notifyMessageStatus(sms, App.STATUS_FAILED,
"Message has too many parts ("+(numParts)+")");
return;
}
String packageName = app.chooseOutgoingSmsPackage(numParts);
if (packageName == null)
{
nextValidOutgoingTime = app.getNextValidOutgoingTime(numParts);
if (nextValidOutgoingTime <= now) // should never happen if (nextValidOutgoingTime <= now) // should never happen
{ {
@ -289,9 +266,11 @@ public class Outbox {
outgoingQueue.poll(); outgoingQueue.poll();
numSendingOutgoingMessages++; numSendingOutgoingMessages++;
sms.setProcessingState(OutgoingMessage.ProcessingState.Sending); message.setProcessingState(OutgoingMessage.ProcessingState.Sending);
message.send(schedule);
message.setSendTimeout();
sms.trySend(bodyParts, packageName);
notifyChanged(); notifyChanged();
} }
} }

View File

@ -1,12 +1,16 @@
package org.envaya.sms; package org.envaya.sms;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.content.Context;
import org.envaya.sms.receiver.OutgoingMessageRetry; import org.envaya.sms.receiver.OutgoingMessageRetry;
import android.content.Intent; import android.content.Intent;
import android.net.Uri; import android.net.Uri;
import java.util.ArrayList; import android.os.SystemClock;
import org.envaya.sms.receiver.OutgoingMessageTimeout;
public class OutgoingMessage extends QueuedMessage { public abstract class OutgoingMessage extends QueuedMessage {
private String serverId; private String serverId;
private String message; private String message;
@ -18,6 +22,12 @@ public class OutgoingMessage extends QueuedMessage {
private ProcessingState state = ProcessingState.None; private ProcessingState state = ProcessingState.None;
public class ScheduleInfo
{
public boolean now = false;
public long time = 0;
}
public enum ProcessingState public enum ProcessingState
{ {
None, // not doing anything with this sms now... just sitting around None, // not doing anything with this sms now... just sitting around
@ -43,6 +53,13 @@ public class OutgoingMessage extends QueuedMessage {
this.state = status; this.state = status;
} }
public boolean isCancelable()
{
return this.state == ProcessingState.None
|| this.state == ProcessingState.Queued
|| this.state == ProcessingState.Scheduled;
}
static synchronized int getNextLocalId() static synchronized int getNextLocalId()
{ {
return nextLocalId++; return nextLocalId++;
@ -59,9 +76,9 @@ public class OutgoingMessage extends QueuedMessage {
("_o" + localId) : serverId)); ("_o" + localId) : serverId));
} }
public String getLogName() public static Uri getUriForServerId(String serverId)
{ {
return (serverId == null) ? "SMS reply" : ("SMS id=" + serverId); return Uri.withAppendedPath(App.OUTGOING_URI, serverId);
} }
public String getServerId() public String getServerId()
@ -114,31 +131,6 @@ public class OutgoingMessage extends QueuedMessage {
return priority; return priority;
} }
public void trySend(ArrayList<String> bodyParts, String packageName)
{
if (numRetries == 0)
{
app.log("Sending " + getLogName() + " to " + getTo());
}
else
{
app.log("Retrying sending " + getLogName() + " to " + getTo());
}
int numParts = bodyParts.size();
if (numParts > 1)
{
app.log("(Multipart message with "+numParts+" parts)");
}
Intent intent = new Intent(packageName + App.OUTGOING_SMS_INTENT_SUFFIX, this.getUri());
intent.putExtra(App.OUTGOING_SMS_EXTRA_DELIVERY_REPORT, false);
intent.putExtra(App.OUTGOING_SMS_EXTRA_TO, getTo());
intent.putExtra(App.OUTGOING_SMS_EXTRA_BODY, bodyParts);
app.sendBroadcast(intent, "android.permission.SEND_SMS");
}
protected Intent getRetryIntent() { protected Intent getRetryIntent() {
Intent intent = new Intent(app, OutgoingMessageRetry.class); Intent intent = new Intent(app, OutgoingMessageRetry.class);
intent.setData(this.getUri()); intent.setData(this.getUri());
@ -165,8 +157,42 @@ public class OutgoingMessage extends QueuedMessage {
return getDisplayType() + " to " + getTo(); return getDisplayType() + " to " + getTo();
} }
public String getDisplayType() abstract String getLogName();
public void validate() throws ValidationException
{ {
return "SMS"; }
abstract ScheduleInfo scheduleSend();
abstract void send(ScheduleInfo schedule);
protected PendingIntent getTimeoutPendingIntent()
{
Intent timeout = new Intent(app, OutgoingMessageTimeout.class);
timeout.setData(getUri());
return PendingIntent.getBroadcast(app,
0,
timeout,
0);
}
public void setSendTimeout()
{
AlarmManager alarm = (AlarmManager) app.getSystemService(Context.ALARM_SERVICE);
PendingIntent timeoutIntent = getTimeoutPendingIntent();
alarm.set(AlarmManager.ELAPSED_REALTIME_WAKEUP,
SystemClock.elapsedRealtime() + App.MESSAGE_SEND_TIMEOUT, timeoutIntent);
}
public void clearSendTimeout()
{
AlarmManager alarm = (AlarmManager) app.getSystemService(Context.ALARM_SERVICE);
PendingIntent timeoutIntent = getTimeoutPendingIntent();
alarm.cancel(timeoutIntent);
} }
} }

View File

@ -0,0 +1,127 @@
package org.envaya.sms;
import android.content.Intent;
import android.telephony.SmsManager;
import java.util.ArrayList;
public class OutgoingSms extends OutgoingMessage {
public OutgoingSms(App app)
{
super(app);
}
public String getLogName()
{
return "SMS";
}
private ArrayList<String> _bodyParts;
public ArrayList<String> getBodyParts()
{
if (_bodyParts == null)
{
SmsManager smgr = SmsManager.getDefault();
_bodyParts = smgr.divideMessage(getMessageBody());
}
return _bodyParts;
}
public int getNumParts()
{
return getBodyParts().size();
}
public class ScheduleInfo extends OutgoingMessage.ScheduleInfo
{
public String packageName;
}
public OutgoingMessage.ScheduleInfo scheduleSend()
{
ScheduleInfo schedule = new ScheduleInfo();
int numParts = getNumParts();
String packageName = app.chooseOutgoingSmsPackage(numParts);
if (packageName == null)
{
schedule.time = app.getNextValidOutgoingTime(numParts);
schedule.now = false;
}
else
{
schedule.now = true;
schedule.packageName = packageName;
}
return schedule;
}
public void send(OutgoingMessage.ScheduleInfo _schedule)
{
ScheduleInfo schedule = (ScheduleInfo)_schedule;
if (numRetries == 0)
{
app.log("Sending " + getDescription());
}
else
{
app.log("Retrying sending " + getDescription());
}
ArrayList<String> bodyParts = getBodyParts();
int numParts = bodyParts.size();
if (numParts > 1)
{
app.log("(Multipart message with "+numParts+" parts)");
}
Intent intent = new Intent(schedule.packageName + App.OUTGOING_SMS_INTENT_SUFFIX, this.getUri());
intent.putExtra(App.OUTGOING_SMS_EXTRA_DELIVERY_REPORT, false);
intent.putExtra(App.OUTGOING_SMS_EXTRA_TO, getTo());
intent.putExtra(App.OUTGOING_SMS_EXTRA_BODY, bodyParts);
app.sendBroadcast(intent, "android.permission.SEND_SMS");
}
public String getDisplayType()
{
return "SMS";
}
@Override
public void validate() throws ValidationException
{
super.validate();
String to = getTo();
if (to == null || to.length() == 0)
{
throw new ValidationException("Destination address is empty");
}
if (!app.isForwardablePhoneNumber(to))
{
// this is mostly to prevent accidentally sending real messages to
// random people while testing...
throw new ValidationException("Destination address is not allowed");
}
String messageBody = getMessageBody();
if (messageBody == null || messageBody.length() == 0)
{
throw new ValidationException("Message body is empty");
}
int numParts = getNumParts();
if (numParts > App.OUTGOING_SMS_MAX_COUNT)
{
throw new ValidationException("Message has too many parts ("+numParts+")");
}
}
}

View File

@ -0,0 +1,9 @@
package org.envaya.sms;
public class ValidationException extends Exception {
public ValidationException(String message)
{
super(message);
}
}

View File

@ -0,0 +1,41 @@
package org.envaya.sms;
import java.io.IOException;
import java.io.InputStream;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import org.apache.http.HttpResponse;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
public class XmlUtils {
public static Document parseResponse(HttpResponse response)
throws IOException, ParserConfigurationException, SAXException {
InputStream responseStream = response.getEntity().getContent();
DocumentBuilder xmlBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
return xmlBuilder.parse(responseStream);
}
public static String getElementText(Element element) {
StringBuilder text = new StringBuilder();
NodeList childNodes = element.getChildNodes();
int numChildren = childNodes.getLength();
for (int j = 0; j < numChildren; j++) {
text.append(childNodes.item(j).getNodeValue());
}
return text.toString();
}
public static String getErrorText(Document xml) {
NodeList errorNodes = xml.getElementsByTagName("error");
if (errorNodes.getLength() > 0) {
Element errorElement = (Element) errorNodes.item(0);
return getElementText(errorElement);
}
return null;
}
}

View File

@ -12,11 +12,16 @@ public class ExpansionPackInstallReceiver extends BroadcastReceiver
{ {
App app = (App) context.getApplicationContext(); App app = (App) context.getApplicationContext();
String action = intent.getAction();
String packageName = intent.getData().getSchemeSpecificPart(); String packageName = intent.getData().getSchemeSpecificPart();
if (packageName != null && packageName.startsWith(context.getPackageName() + ".pack")) if (packageName != null)
{
if (packageName.startsWith(context.getPackageName() + ".pack"))
{ {
app.updateExpansionPacks(); app.updateExpansionPacks();
} }
} }
}
} }

View File

@ -0,0 +1,29 @@
package org.envaya.sms.receiver;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import org.envaya.sms.App;
import org.envaya.sms.OutgoingMessage;
public class OutgoingMessageTimeout extends BroadcastReceiver
{
@Override
public void onReceive(Context context, Intent intent)
{
App app = (App) context.getApplicationContext();
if (!app.isEnabled())
{
return;
}
OutgoingMessage message = app.outbox.getMessage(intent.getData());
if (message == null)
{
return;
}
app.outbox.messageFailed(message, "Timeout while attempting to send message");
}
}

View File

@ -0,0 +1,200 @@
package org.envaya.sms.task;
import android.os.AsyncTask;
import android.os.Build;
import org.envaya.sms.App;
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.apache.http.Header;
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.mime.FormBodyPart;
import org.apache.http.entity.mime.MultipartEntity;
import org.apache.http.entity.mime.content.StringBody;
import org.apache.http.message.BasicNameValuePair;
public class BaseHttpTask extends AsyncTask<String, Void, HttpResponse> {
protected App app;
protected String url;
protected List<BasicNameValuePair> params = new ArrayList<BasicNameValuePair>();
private List<FormBodyPart> formParts;
protected boolean useMultipartPost = false;
protected HttpPost post;
protected Throwable requestException;
public BaseHttpTask(App app, String url, BasicNameValuePair... paramsArr)
{
this.url = url;
this.app = app;
params = new ArrayList<BasicNameValuePair>(Arrays.asList(paramsArr));
}
public void addParam(String name, String value)
{
params.add(new BasicNameValuePair(name, value));
}
public void setFormParts(List<FormBodyPart> formParts)
{
useMultipartPost = true;
this.formParts = formParts;
}
protected HttpPost makeHttpPost() throws Exception
{
HttpPost httpPost = new HttpPost(url);
httpPost.setHeader("User-Agent", "EnvayaSMS/" + app.getPackageInfo().versionName + " (Android; SDK "+Build.VERSION.SDK_INT + "; " + Build.MANUFACTURER + "; " + Build.MODEL+")");
if (useMultipartPost)
{
MultipartEntity entity = new MultipartEntity();//HttpMultipartMode.BROWSER_COMPATIBLE);
Charset charset = Charset.forName("UTF-8");
for (BasicNameValuePair param : params)
{
entity.addPart(param.getName(), new StringBody(param.getValue(), charset));
}
for (FormBodyPart formPart : formParts)
{
entity.addPart(formPart);
}
httpPost.setEntity(entity);
}
else
{
httpPost.setEntity(new UrlEncodedFormEntity(params, "UTF-8"));
}
return httpPost;
}
protected HttpResponse doInBackground(String... ignored)
{
try
{
post = makeHttpPost();
HttpClient client = app.getHttpClient();
return client.execute(post);
}
catch (Throwable ex)
{
requestException = ex;
try
{
String message = ex.getMessage();
// workaround for https://issues.apache.org/jira/browse/HTTPCLIENT-881
if ((ex instanceof IOException)
&& message != null && message.equals("Connection already shutdown"))
{
//app.log("Retrying request");
post = makeHttpPost();
HttpClient client = app.getHttpClient();
return client.execute(post);
}
}
catch (Throwable ex2)
{
requestException = ex2;
}
}
return null;
}
public boolean isValidContentType(String contentType)
{
return true; // contentType.startsWith("text/xml");
}
@Override
protected void onPostExecute(HttpResponse response) {
if (response != null)
{
try
{
int statusCode = response.getStatusLine().getStatusCode();
Header contentTypeHeader = response.getFirstHeader("Content-Type");
String contentType = (contentTypeHeader != null) ? contentTypeHeader.getValue() : "";
boolean validContentType = isValidContentType(contentType);
if (statusCode == 200)
{
if (validContentType)
{
handleResponse(response);
}
else
{
throw new Exception("Invalid response type " + contentType);
}
}
else if (statusCode >= 400 && statusCode <= 499 && validContentType)
{
handleErrorResponse(response);
handleFailure();
}
else
{
throw new Exception("HTTP " + statusCode);
}
}
catch (Throwable ex)
{
post.abort();
handleResponseException(ex);
handleFailure();
}
try
{
response.getEntity().consumeContent();
}
catch (IOException ex)
{
}
}
else
{
handleRequestException(requestException);
handleFailure();
}
}
protected void handleResponse(HttpResponse response) throws Exception
{
// if we get a valid server response after a connectivity error, then forward any pending messages
if (app.hasConnectivityError())
{
app.onConnectivityRestored();
}
}
protected void handleErrorResponse(HttpResponse response) throws Exception
{
}
protected void handleFailure()
{
}
protected void handleRequestException(Throwable ex)
{
}
protected void handleResponseException(Throwable ex)
{
}
}

View File

@ -0,0 +1,101 @@
package org.envaya.sms.task;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.net.ConnectivityManager;
import android.net.wifi.WifiManager;
import android.os.AsyncTask;
import android.os.SystemClock;
import org.envaya.sms.App;
import org.envaya.sms.receiver.ReenableWifiReceiver;
import java.io.IOException;
import java.net.InetAddress;
public class CheckConnectivityTask extends AsyncTask<String, Void, Boolean> {
protected App app;
protected String hostName;
protected int networkType;
public CheckConnectivityTask(App app, String hostName, int networkType)
{
this.app = app;
this.hostName = hostName;
this.networkType = networkType;
}
protected Boolean doInBackground(String... ignored)
{
try
{
InetAddress addr = InetAddress.getByName(hostName);
if (addr.isReachable(App.HTTP_CONNECTION_TIMEOUT))
{
return true;
}
}
catch (IOException ex)
{
// just what we suspected...
// server not reachable on this interface
}
return false;
}
@Override
protected void onPostExecute(Boolean reachable)
{
if (reachable.booleanValue())
{
app.log("OK");
app.onConnectivityRestored();
}
else
{
app.log("Can't connect to "+hostName+".");
WifiManager wmgr = (WifiManager)app.getSystemService(Context.WIFI_SERVICE);
if (!app.isNetworkFailoverEnabled())
{
app.debug("Network failover disabled.");
}
else if (networkType == ConnectivityManager.TYPE_WIFI)
{
app.log("Switching from WIFI to MOBILE");
PendingIntent pendingIntent = PendingIntent.getBroadcast(app,
0,
new Intent(app, ReenableWifiReceiver.class),
0);
// set an alarm to try restoring Wi-Fi in a little while
AlarmManager alarm =
(AlarmManager)app.getSystemService(Context.ALARM_SERVICE);
alarm.set(
AlarmManager.ELAPSED_REALTIME_WAKEUP,
SystemClock.elapsedRealtime() + App.DISABLE_WIFI_INTERVAL,
pendingIntent);
wmgr.setWifiEnabled(false);
}
else if (networkType == ConnectivityManager.TYPE_MOBILE
&& !wmgr.isWifiEnabled())
{
app.log("Switching from MOBILE to WIFI");
wmgr.setWifiEnabled(true);
}
else
{
app.log("Can't automatically fix connectivity.");
}
}
}
}

View File

@ -2,7 +2,6 @@ package org.envaya.sms.task;
import org.apache.http.HttpResponse; import org.apache.http.HttpResponse;
import org.apache.http.message.BasicNameValuePair; import org.apache.http.message.BasicNameValuePair;
import org.envaya.sms.App;
import org.envaya.sms.IncomingMessage; import org.envaya.sms.IncomingMessage;
import org.envaya.sms.OutgoingMessage; import org.envaya.sms.OutgoingMessage;
@ -13,10 +12,12 @@ public class ForwarderTask extends HttpTask {
public ForwarderTask(IncomingMessage message, BasicNameValuePair... paramsArr) { public ForwarderTask(IncomingMessage message, BasicNameValuePair... paramsArr) {
super(message.app, paramsArr); super(message.app, paramsArr);
this.message = message; this.message = message;
}
params.add(new BasicNameValuePair("action", App.ACTION_INCOMING)); @Override
params.add(new BasicNameValuePair("from", message.getFrom())); public boolean isValidContentType(String contentType)
params.add(new BasicNameValuePair("timestamp", "" + message.getTimestamp())); {
return contentType.startsWith("text/xml");
} }
@Override @Override
@ -31,6 +32,8 @@ public class ForwarderTask extends HttpTask {
app.outbox.sendMessage(reply); app.outbox.sendMessage(reply);
} }
app.inbox.messageForwarded(message); app.inbox.messageForwarded(message);
super.handleResponse(response);
} }
@Override @Override

View File

@ -4,78 +4,56 @@
*/ */
package org.envaya.sms.task; package org.envaya.sms.task;
import android.os.AsyncTask; import org.envaya.sms.XmlUtils;
import org.envaya.sms.OutgoingSms;
import org.envaya.sms.OutgoingMessage;
import org.envaya.sms.App;
import org.envaya.sms.Base64Coder;
import android.content.SharedPreferences;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.preference.PreferenceManager;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException; import java.io.UnsupportedEncodingException;
import java.net.SocketTimeoutException; import java.net.SocketTimeoutException;
import java.net.UnknownHostException; import java.net.UnknownHostException;
import java.nio.charset.Charset;
import java.security.MessageDigest; import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.Comparator; import java.util.Comparator;
import java.util.List; import java.util.List;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException; import javax.xml.parsers.ParserConfigurationException;
import org.apache.http.HttpResponse; import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpPost; import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.mime.FormBodyPart;
import org.apache.http.entity.mime.MultipartEntity;
import org.apache.http.entity.mime.content.StringBody;
import org.apache.http.message.BasicNameValuePair; import org.apache.http.message.BasicNameValuePair;
import org.envaya.sms.App;
import org.envaya.sms.Base64Coder;
import org.envaya.sms.OutgoingMessage;
import org.w3c.dom.Document; import org.w3c.dom.Document;
import org.w3c.dom.Element; import org.w3c.dom.Element;
import org.w3c.dom.NodeList; import org.w3c.dom.NodeList;
import org.xml.sax.SAXException; import org.xml.sax.SAXException;
public class HttpTask extends AsyncTask<String, Void, HttpResponse> { public class HttpTask extends BaseHttpTask {
protected App app;
protected String url;
protected List<BasicNameValuePair> params = new ArrayList<BasicNameValuePair>();
protected BasicNameValuePair[] paramsArr;
private List<FormBodyPart> formParts;
private boolean useMultipartPost = false;
private HttpPost post;
private String logEntries; private String logEntries;
private boolean retryOnConnectivityError; private boolean retryOnConnectivityError;
private BasicNameValuePair[] ctorParams;
public HttpTask(App app, BasicNameValuePair... paramsArr) public HttpTask(App app, BasicNameValuePair... paramsArr)
{ {
super(); super(app, app.getServerUrl(), paramsArr);
this.app = app; this.ctorParams = paramsArr;
this.paramsArr = paramsArr;
params = new ArrayList<BasicNameValuePair>(Arrays.asList(paramsArr));
} }
public void setRetryOnConnectivityError(boolean retry) public void setRetryOnConnectivityError(boolean retry)
{ {
this.retryOnConnectivityError = retry; this.retryOnConnectivityError = retry; // doesn't work with addParam!
} }
protected HttpTask getCopy() protected HttpTask getCopy()
{ {
return new HttpTask(app, paramsArr); return new HttpTask(app, ctorParams); // doesn't work with addParam!
}
public void setFormParts(List<FormBodyPart> formParts)
{
useMultipartPost = true;
this.formParts = formParts;
} }
private String getSignature() private String getSignature()
@ -110,6 +88,7 @@ public class HttpTask extends AsyncTask<String, Void, HttpResponse> {
return new String(Base64Coder.encode(digest)); return new String(Base64Coder.encode(digest));
} }
@Override
protected HttpResponse doInBackground(String... ignored) { protected HttpResponse doInBackground(String... ignored) {
url = app.getServerUrl(); url = app.getServerUrl();
@ -122,88 +101,33 @@ public class HttpTask extends AsyncTask<String, Void, HttpResponse> {
params.add(new BasicNameValuePair("version", "" + app.getPackageInfo().versionCode)); params.add(new BasicNameValuePair("version", "" + app.getPackageInfo().versionCode));
params.add(new BasicNameValuePair("phone_number", app.getPhoneNumber())); params.add(new BasicNameValuePair("phone_number", app.getPhoneNumber()));
params.add(new BasicNameValuePair("send_limit", "" + app.getOutgoingMessageLimit()));
ConnectivityManager cm =
(ConnectivityManager)app.getSystemService(App.CONNECTIVITY_SERVICE);
NetworkInfo activeNetwork = cm.getActiveNetworkInfo();
if (activeNetwork != null)
{
params.add(new BasicNameValuePair("network", "" + activeNetwork.getTypeName()));
}
params.add(new BasicNameValuePair("log", logEntries)); params.add(new BasicNameValuePair("log", logEntries));
post = new HttpPost(url); return super.doInBackground();
try
{
if (useMultipartPost)
{
MultipartEntity entity = new MultipartEntity();//HttpMultipartMode.BROWSER_COMPATIBLE);
Charset charset = Charset.forName("UTF-8");
for (BasicNameValuePair param : params)
{
entity.addPart(param.getName(), new StringBody(param.getValue(), charset));
} }
for (FormBodyPart formPart : formParts) @Override
protected HttpPost makeHttpPost()
throws Exception
{ {
entity.addPart(formPart); HttpPost httpPost = super.makeHttpPost();
}
post.setEntity(entity);
}
else
{
post.setEntity(new UrlEncodedFormEntity(params, "UTF-8"));
}
HttpClient client = app.getHttpClient();
String signature = getSignature(); String signature = getSignature();
post.setHeader("X-Request-Signature", signature); httpPost.setHeader("X-Request-Signature", signature);
post.setHeader("User-Agent", "EnvayaSMS/" + app.getPackageInfo().versionName);
HttpResponse response = client.execute(post); return httpPost;
int statusCode = response.getStatusLine().getStatusCode();
if (statusCode == 200)
{
return response;
}
else if (statusCode == 403)
{
response.getEntity().consumeContent();
app.ungetNewLogEntries(logEntries);
app.log("Failed to authenticate to server");
app.log("(Phone number or password may be incorrect)");
return null;
}
else
{
response.getEntity().consumeContent();
app.ungetNewLogEntries(logEntries);
app.log("Received HTTP " + statusCode + " from server");
return null;
}
}
catch (IOException ex)
{
post.abort();
app.ungetNewLogEntries(logEntries);
app.logError("Error while contacting server", ex);
if (ex instanceof UnknownHostException || ex instanceof SocketTimeoutException)
{
if (retryOnConnectivityError)
{
app.addQueuedTask(getCopy());
}
app.asyncCheckConnectivity();
}
return null;
}
catch (Throwable ex)
{
post.abort();
app.ungetNewLogEntries(logEntries);
app.logError("Unexpected error while contacting server", ex, true);
return null;
}
} }
protected String getDefaultToAddress() protected String getDefaultToAddress()
@ -215,33 +139,36 @@ public class HttpTask extends AsyncTask<String, Void, HttpResponse> {
throws IOException, ParserConfigurationException, SAXException throws IOException, ParserConfigurationException, SAXException
{ {
List<OutgoingMessage> messages = new ArrayList<OutgoingMessage>(); List<OutgoingMessage> messages = new ArrayList<OutgoingMessage>();
InputStream responseStream = response.getEntity().getContent(); Document xml = XmlUtils.parseResponse(response);
DocumentBuilder xmlBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
Document xml = xmlBuilder.parse(responseStream);
NodeList smsNodes = xml.getElementsByTagName("sms"); Element messagesElement = (Element) xml.getElementsByTagName("messages").item(0);
for (int i = 0; i < smsNodes.getLength(); i++) { if (messagesElement != null)
Element smsElement = (Element) smsNodes.item(i); {
NodeList messageNodes = messagesElement.getChildNodes();
int numNodes = messageNodes.getLength();
for (int i = 0; i < numNodes; i++)
{
Element messageElement = (Element) messageNodes.item(i);
OutgoingMessage sms = new OutgoingMessage(app); OutgoingMessage message = new OutgoingSms(app);
sms.setFrom(app.getPhoneNumber()); message.setFrom(app.getPhoneNumber());
String to = smsElement.getAttribute("to"); String to = messageElement.getAttribute("to");
sms.setTo(to.equals("") ? getDefaultToAddress() : to); message.setTo(to.equals("") ? getDefaultToAddress() : to);
String serverId = smsElement.getAttribute("id"); String serverId = messageElement.getAttribute("id");
sms.setServerId(serverId.equals("") ? null : serverId); message.setServerId(serverId.equals("") ? null : serverId);
String priorityStr = smsElement.getAttribute("priority"); String priorityStr = messageElement.getAttribute("priority");
if (!priorityStr.equals("")) if (!priorityStr.equals(""))
{ {
try try
{ {
sms.setPriority(Integer.parseInt(priorityStr)); message.setPriority(Integer.parseInt(priorityStr));
} }
catch (NumberFormatException ex) catch (NumberFormatException ex)
{ {
@ -249,54 +176,61 @@ public class HttpTask extends AsyncTask<String, Void, HttpResponse> {
} }
} }
StringBuilder messageBody = new StringBuilder(); message.setMessageBody(XmlUtils.getElementText(messageElement));
NodeList childNodes = smsElement.getChildNodes();
int numChildren = childNodes.getLength(); messages.add(message);
for (int j = 0; j < numChildren; j++)
{
messageBody.append(childNodes.item(j).getNodeValue());
} }
sms.setMessageBody(messageBody.toString());
messages.add(sms);
} }
return messages; return messages;
} }
@Override @Override
protected void onPostExecute(HttpResponse response) { protected void handleFailure()
if (response != null)
{ {
try app.ungetNewLogEntries(logEntries);
{
handleResponse(response);
} }
catch (Throwable ex)
@Override
protected void handleResponseException(Throwable ex)
{ {
post.abort(); app.logError("Error in server response", ex);
app.logError("Error processing server response", ex);
handleFailure();
} }
try
@Override
protected void handleRequestException(Throwable ex)
{ {
response.getEntity().consumeContent(); if (ex instanceof IOException)
{
app.logError("Error while contacting server", ex);
if (ex instanceof UnknownHostException || ex instanceof SocketTimeoutException)
{
if (retryOnConnectivityError)
{
app.addQueuedTask(getCopy());
} }
catch (IOException ex)
{ app.onConnectivityError();
} }
} }
else else
{ {
handleFailure(); app.logError("Unexpected error while contacting server", ex, true);
} }
} }
protected void handleResponse(HttpResponse response) throws Exception @Override
public void handleErrorResponse(HttpResponse response) throws Exception
{ {
Document xml = XmlUtils.parseResponse(response);
String error = XmlUtils.getErrorText(xml);
if (error != null)
{
app.log(error);
} }
else
protected void handleFailure()
{ {
app.log("HTTP " +response.getStatusLine().getStatusCode());
}
} }
} }

View File

@ -12,6 +12,12 @@ public class PollerTask extends HttpTask {
super(app, new BasicNameValuePair("action", App.ACTION_OUTGOING)); super(app, new BasicNameValuePair("action", App.ACTION_OUTGOING));
} }
@Override
public boolean isValidContentType(String contentType)
{
return contentType.startsWith("text/xml");
}
@Override @Override
protected void onPostExecute(HttpResponse response) { protected void onPostExecute(HttpResponse response) {
super.onPostExecute(response); super.onPostExecute(response);

View File

@ -1,14 +1,22 @@
package org.envaya.sms.ui; package org.envaya.sms.ui;
import android.app.Activity; import android.app.Activity;
import android.app.AlertDialog;
import android.content.DialogInterface;
import android.content.DialogInterface.OnClickListener;
import android.content.Intent;
import android.os.Bundle; import android.os.Bundle;
import android.preference.PreferenceManager;
import android.text.Html; import android.text.Html;
import android.view.View;
import android.widget.TextView; import android.widget.TextView;
import org.envaya.sms.App; import org.envaya.sms.App;
import org.envaya.sms.R; import org.envaya.sms.R;
public class Help extends Activity { public class Help extends Activity {
private App app;
@Override @Override
public void onCreate(Bundle icicle) { public void onCreate(Bundle icicle) {
super.onCreate(icicle); super.onCreate(icicle);
@ -17,19 +25,44 @@ public class Help extends Activity {
TextView help = (TextView) this.findViewById(R.id.help); TextView help = (TextView) this.findViewById(R.id.help);
App app = (App)getApplication(); app = (App)getApplication();
String html = "<b>EnvayaSMS " + app.getPackageInfo().versionName + "</b><br /><br />" String html = "<b>EnvayaSMS " + app.getPackageInfo().versionName + "</b><br /><br />"
+ "EnvayaSMS is a SMS gateway.<br /><br /> "
+ "It forwards all incoming SMS messages received by this phone to a server on the internet, "
+ "and also sends outgoing SMS messages from that server to other phones.<br /><br />"
+ "See sms.envaya.org for more information.<br /><br />"
+ "The Settings screen allows you configure EnvayaSMS to work with a particular server, "
+ "by entering the server URL, your phone number, "
+ "and the password assigned to your phone on the server.<br /><br />"
+ "Menu icons cc/by www.androidicons.com<br /><br />"; + "Menu icons cc/by www.androidicons.com<br /><br />";
help.setText(Html.fromHtml(html)); help.setText(Html.fromHtml(html));
} }
public void resetClicked(View v)
{
new AlertDialog.Builder(this)
.setTitle("Are you sure?")
.setPositiveButton("Yes",
new OnClickListener() {
public void onClick(DialogInterface dialog, int which)
{
PreferenceManager.getDefaultSharedPreferences(app)
.edit()
.clear()
.commit();
app.enabledChanged();
dialog.dismiss();
finish();
}
}
)
.setNegativeButton("Cancel",
new OnClickListener() {
public void onClick(DialogInterface dialog, int which)
{
dialog.dismiss();
}
}
)
.show();
}
} }

View File

@ -2,13 +2,11 @@ package org.envaya.sms.ui;
import org.envaya.sms.task.HttpTask; import org.envaya.sms.task.HttpTask;
import android.app.Activity; import android.app.Activity;
import android.content.BroadcastReceiver; import android.content.*;
import android.content.Context; import android.net.Uri;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Bundle; import android.os.Bundle;
import android.preference.PreferenceManager; import android.preference.PreferenceManager;
import android.text.method.ScrollingMovementMethod; import android.text.method.LinkMovementMethod;
import android.view.Menu; import android.view.Menu;
import android.view.MenuInflater; import android.view.MenuInflater;
import android.view.MenuItem; import android.view.MenuItem;
@ -20,7 +18,7 @@ import org.apache.http.message.BasicNameValuePair;
import org.envaya.sms.App; import org.envaya.sms.App;
import org.envaya.sms.R; import org.envaya.sms.R;
public class Main extends Activity { public class LogView extends Activity {
private App app; private App app;
@ -37,13 +35,12 @@ public class Main extends Activity {
private class TestTask extends HttpTask private class TestTask extends HttpTask
{ {
public TestTask() { public TestTask() {
super(Main.this.app, new BasicNameValuePair("action", App.ACTION_TEST)); super(LogView.this.app, new BasicNameValuePair("action", App.ACTION_TEST));
} }
@Override @Override
protected void handleResponse(HttpResponse response) throws Exception protected void handleResponse(HttpResponse response) throws Exception
{ {
parseResponseXML(response);
app.log("Server connection OK!"); app.log("Server connection OK!");
} }
} }
@ -85,13 +82,15 @@ public class Main extends Activity {
app = (App) getApplication(); app = (App) getApplication();
setContentView(R.layout.main); setContentView(R.layout.log_view);
PreferenceManager.setDefaultValues(this, R.xml.prefs, false); PreferenceManager.setDefaultValues(this, R.xml.prefs, false);
scrollView = (ScrollView) this.findViewById(R.id.info_scroll); scrollView = (ScrollView) this.findViewById(R.id.info_scroll);
info = (TextView) this.findViewById(R.id.info); info = (TextView) this.findViewById(R.id.info);
info.setMovementMethod(new ScrollingMovementMethod()); info.setMovementMethod(LinkMovementMethod.getInstance());
//info.setMovementMethod(new ScrollingMovementMethod());
updateLogView(); updateLogView();

View File

@ -89,6 +89,10 @@ public class Prefs extends PreferenceActivity implements OnSharedPreferenceChang
app.log("Server URL changed to: " + app.getDisplayString(app.getServerUrl())); app.log("Server URL changed to: " + app.getDisplayString(app.getServerUrl()));
} }
else if (key.equals("call_notifications"))
{
app.log("Call notifications changed to: " + (app.callNotificationsEnabled() ? "ON": "OFF"));
}
else if (key.equals("phone_number")) else if (key.equals("phone_number"))
{ {
app.log("Phone number changed to: " + app.getDisplayString(app.getPhoneNumber())); app.log("Phone number changed to: " + app.getDisplayString(app.getPhoneNumber()));