5
0
mirror of https://github.com/cwinfo/envayasms.git synced 2024-09-08 01:24:31 +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"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.envaya.sms"
android:versionCode="17"
android:versionName="2.0.4">
android:versionCode="18"
android:versionName="2.0.5">
<uses-sdk android:minSdkVersion="4" />
@ -10,6 +10,7 @@
<uses-permission android:name="android.permission.CHANGE_NETWORK_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.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.RECEIVE_SMS" />
<uses-permission android:name="android.permission.RECEIVE_MMS" />
<uses-permission android:name="android.permission.SEND_SMS" />
@ -22,13 +23,14 @@
<application android:name="org.envaya.sms.App"
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>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity android:name=".ui.Help" android:label="EnvayaSMS : Help">
</activity>
@ -81,6 +83,9 @@
<receiver android:name=".receiver.DequeueOutgoingMessageReceiver">
</receiver>
<receiver android:name=".receiver.OutgoingMessageTimeout">
</receiver>
<receiver android:name=".receiver.OutgoingMessagePoller">
</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.

164
build.xml
View File

@ -1,79 +1,85 @@
<?xml version="1.0" encoding="UTF-8"?>
<project name="EnvayaSMS" default="help">
<!-- The local.properties file is created and updated by the 'android'
tool.
It contains the path to the SDK. It should *NOT* be checked into
Version Control Systems. -->
<property file="local.properties" />
<!-- The build.properties file can be created by you and is never touched
by the 'android' tool. This is the place to change some of the
default property values used by the Ant rules.
Here are some properties you may want to change/update:
source.dir
The name of the source directory. Default is 'src'.
out.dir
The name of the output directory. Default is 'bin'.
Properties related to the SDK location or the project target should
be updated using the 'android' tool with the 'update' action.
This file is an integral part of the build system for your
application and should be checked into Version Control Systems.
-->
<property file="build.properties" />
<!-- The default.properties file is created and updated by the 'android'
tool, as well as ADT.
This file is an integral part of the build system for your
application and should be checked into Version Control Systems. -->
<property file="default.properties" />
<!-- Required pre-setup import -->
<import file="${sdk.dir}/tools/ant/pre_setup.xml" />
<!-- extension targets. Uncomment the ones where you want to do custom work
in between standard targets -->
<!--
<target name="-pre-build">
</target>
<target name="-pre-compile">
</target>
[This is typically used for code obfuscation.
Compiled code location: ${out.classes.absolute.dir}
If this is not done in place, override ${out.dex.input.absolute.dir}]
<target name="-post-compile">
</target>
-->
<!-- Execute the Android Setup task that will setup some properties
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:
- Customize only one target:
- copy/paste the target into this file, *before* the
<setup> task.
- customize it to your needs.
- Customize the whole script.
- copy/paste the content of the rules files (minus the top node)
into this file, *after* the <setup> task
- disable the import of the rules by changing the setup task
below to <setup import="false" />.
- customize to your needs.
-->
<setup />
</project>
<?xml version="1.0" encoding="UTF-8"?>
<project name="EnvayaSMS" default="help">
<!-- The local.properties file is created and updated by the 'android' tool.
It contains the path to the SDK. It should *NOT* be checked into
Version Control Systems. -->
<property file="local.properties" />
<!-- The ant.properties file can be created by you. It is only edited by the
'android' tool to add properties to it.
This is the place to change some Ant specific build properties.
Here are some properties you may want to change/update:
source.dir
The name of the source directory. Default is 'src'.
out.dir
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
be updated using the 'android' tool with the 'update' action.
This file is an integral part of the build system for your
application and should be checked into Version Control Systems.
-->
<property file="ant.properties" />
<!-- The project.properties file is created and updated by the 'android'
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
application and should be checked into Version Control Systems. -->
<loadproperties srcFile="project.properties" />
<!-- quick check on sdk.dir -->
<fail
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
in between standard targets -->
<!--
<target name="-pre-build">
</target>
<target name="-pre-compile">
</target>
/* This is typically used for code obfuscation.
Compiled code location: ${out.classes.absolute.dir}
If this is not done in place, override ${out.dex.input.absolute.dir} */
<target name="-post-compile">
</target>
-->
<!-- Import the actual build file.
To customize existing targets, there are two options:
- Customize only one target:
- copy/paste the target into this file, *before* the
<import> task.
- customize it to your needs.
- Customize the whole content of build.xml
- copy/paste the content of the rules files (minus the top node)
into this file, replacing the <import> task.
- 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"
-->
<!-- version-tag: 1 -->
<import file="${sdk.dir}/tools/ant/build.xml" />
</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"
></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
android:key="wifi_sleep_policy"
android:title="Wi-Fi sleep policy"
@ -52,7 +59,7 @@
android:entries="@array/wifi_sleep_policies"
android:entryValues="@array/wifi_sleep_policies_values"
>
</ListPreference>
</ListPreference>
<CheckBoxPreference
android:key="network_failover"

View File

@ -17,14 +17,20 @@ class EnvayaSMS
const STATUS_QUEUED = 'queued';
const STATUS_FAILED = 'failed';
const STATUS_SENT = 'sent';
const STATUS_CANCELLED = 'cancelled';
const DEVICE_STATUS_POWER_CONNECTED = "power_connected";
const DEVICE_STATUS_POWER_DISCONNECTED = "power_disconnected";
const DEVICE_STATUS_BATTERY_LOW = "battery_low";
const DEVICE_STATUS_BATTERY_OKAY = "battery_okay";
const DEVICE_STATUS_SEND_LIMIT_EXCEEDED = "send_limit_exceeded";
const MESSAGE_TYPE_SMS = 'sms';
const MESSAGE_TYPE_MMS = 'mms';
const MESSAGE_TYPE_CALL = 'call';
const NETWORK_MOBILE = "MOBILE";
const NETWORK_WIFI = "WIFI";
static function escape($val)
{
@ -46,7 +52,27 @@ class EnvayaSMS
static::$request = new EnvayaSMS_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
@ -56,12 +82,28 @@ class EnvayaSMS_Request
public $version;
public $phone_number;
public $log;
public $version_name;
public $sdk_int;
public $manufacturer;
public $model;
public $network;
function __construct()
{
$this->version = $_POST['version'];
$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()
@ -124,21 +166,24 @@ class EnvayaSMS_Request
//error_log("Signed data: '$input'");
return base64_encode(sha1($input, true));
}
}
static function get_messages_xml($messages)
{
ob_start();
echo "<?xml version='1.0' encoding='UTF-8'?>\n";
echo "<response>";
echo "<messages>";
foreach ($messages as $message)
{
{
$type = isset($message->type) ? $message->type : EnvayaSMS::MESSAGE_TYPE_SMS;
$id = isset($message->id) ? " id=\"".EnvayaSMS::escape($message->id)."\"" : "";
$to = isset($message->to) ? " to=\"".EnvayaSMS::escape($message->to)."\"" : "";
$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 "</response>";
return ob_get_clean();
}
}
@ -149,6 +194,7 @@ class EnvayaSMS_OutgoingMessage
public $to; // destination phone number
public $message; // content of SMS message
public $priority; // integer priority, higher numbers will be sent first
public $type; // EnvayaSMS::MESSAGE_TYPE_* value (default sms)
}
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 $mms_parts; // array of EnvayaSMS_MMS_Part instances
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)
{
parent::__construct($request);
$this->type = EnvayaSMS::ACTION_INCOMING;
$this->from = $_POST['from'];
$this->message = $_POST['message'];
$this->message = @$_POST['message'];
$this->message_type = $_POST['message_type'];
$this->timestamp = @$_POST['timestamp'];
$this->age = @$_POST['age'];
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 $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)
{

View File

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

View File

@ -14,11 +14,13 @@ $phone_number = $request->phone_number;
$password = @$PASSWORDS[$phone_number];
header("Content-Type: text/xml");
if (!isset($password) || !$request->is_validated($password))
{
header("HTTP/1.1 403 Forbidden");
error_log("Invalid request signature");
echo "Invalid request signature";
error_log("Invalid request signature");
echo EnvayaSMS::get_error_xml("Invalid request signature");
return;
}
@ -37,14 +39,30 @@ $action = $request->get_action();
switch ($action->type)
{
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->message = "You said: {$action->message}";
error_log("Sending reply: {$reply->message}");
header("Content-Type: text/xml");
echo $action->get_response_xml(array($reply));
return;
@ -70,7 +88,6 @@ switch ($action->type)
}
closedir($dir);
header("Content-Type: text/xml");
echo $action->get_response_xml($messages);
return;
@ -81,23 +98,23 @@ switch ($action->type)
// delete file with matching id
if (preg_match('#^\w+$#', $id) && unlink("$OUTGOING_DIR_NAME/$id.json"))
{
echo "OK";
echo EnvayaSMS::get_success_xml();
}
else
{
header("HTTP/1.1 404 Not Found");
echo "invalid id";
echo EnvayaSMS::get_error_xml("Invalid id");
}
return;
case EnvayaSMS::ACTION_DEVICE_STATUS:
error_log("device_status = {$action->status}");
echo "OK";
echo EnvayaSMS::get_success_xml();
return;
case EnvayaSMS::ACTION_TEST:
echo "OK";
echo EnvayaSMS::get_success_xml();
return;
default:
header("HTTP/1.1 404 Not Found");
echo "Invalid action";
echo EnvayaSMS::get_error_xml("Invalid action");
return;
}

View File

@ -14,14 +14,15 @@ import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.net.Uri;
import android.net.wifi.WifiManager;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.SystemClock;
import android.preference.PreferenceManager;
import android.telephony.PhoneStateListener;
import android.telephony.TelephonyManager;
import android.text.Html;
import android.text.SpannableStringBuilder;
import android.util.Log;
import java.io.IOException;
import java.net.InetAddress;
import java.text.DateFormat;
import java.util.ArrayList;
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.HttpProtocolParams;
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.PollerTask;
import org.apache.http.message.BasicNameValuePair;
import org.json.JSONArray;
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_FAILED = "failed";
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_DISCONNECTED = "power_disconnected";
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_SEND_LIMIT_EXCEEDED = "send_limit_exceeded";
public static final String MESSAGE_TYPE_MMS = "mms";
public static final String MESSAGE_TYPE_SMS = "sms";
public static final String MESSAGE_TYPE_CALL = "call";
public static final String LOG_NAME = "EnvayaSMS";
@ -76,9 +81,9 @@ public final class App extends Application {
// signal to PendingMessages activity (if open) that inbox/outbox has changed
public static final String INBOX_CHANGED_INTENT = "org.envaya.sms.INBOX_CHANGED";
public static final String OUTBOX_CHANGED_INTENT = "org.envaya.sms.OUTBOX_CHANGED";
public static final String QUERY_EXPANSION_PACKS_INTENT = "org.envaya.sms.QUERY_EXPANSION_PACKS";
public static final String QUERY_EXPANSION_PACKS_EXTRA_PACKAGES = "packages";
public static final String QUERY_EXPANSION_PACKS_EXTRA_PACKAGES = "packages";
// Interface for sending outgoing messages to expansion packs
public static final String OUTGOING_SMS_INTENT_SUFFIX = ".OUTGOING_SMS";
@ -96,11 +101,13 @@ public final class App extends Application {
public static final String STATUS_EXTRA_NUM_PARTS = "num_parts";
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_SOCKET_TIMEOUT = 60000; // ms
public static final int MESSAGE_SEND_TIMEOUT = 30000; // ms
// Each QueuedMessage is identified within our internal Map by its Uri.
// Currently QueuedMessage instances are only available within EnvayaSMS,
// (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;
// 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
// with a slightly longer check period to account for variance in the time difference
@ -145,7 +152,10 @@ public final class App extends Application {
private int outgoingMessageCount = -1;
private MmsUtils mmsUtils;
private CallListener callListener;
private boolean connectivityError = false;
@Override
public void onCreate()
{
@ -154,6 +164,8 @@ public final class App extends Application {
settings = PreferenceManager.getDefaultSharedPreferences(this);
mmsUtils = new MmsUtils(this);
callListener = new CallListener(this);
outgoingMessagePackages.add(getPackageName());
mmsObserver = new MmsObserver(this);
@ -170,39 +182,87 @@ public final class App extends Application {
}
updateExpansionPacks();
configuredChanged();
}
public void configuredChanged()
{
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: " + getDisplayString(getServerUrl()));
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);
}
}
log("Server URL is: " + getDisplayString(getServerUrl()));
log("Your phone number is: " + getDisplayString(getPhoneNumber()));
if (isTestMode())
{
log("Test mode is ON");
log("Test mode: ON");
log("Test phone numbers:");
for (String sender : getTestPhoneNumbers())
{
log(" " + sender);
}
}
log(Html.fromHtml("<b>To change these settings, click Menu, then Settings.</b>"));
enabledChanged();
log(Html.fromHtml("<b>Press Menu to edit settings.</b>"));
}
}
public void enabledChanged()
{
TelephonyManager telephony = (TelephonyManager)
getSystemService(Context.TELEPHONY_SERVICE);
if (isEnabled())
{
mmsObserver.register();
mmsObserver.register();
telephony.listen(callListener, PhoneStateListener.LISTEN_CALL_STATE);
}
else
{
mmsObserver.unregister();
}
telephony.listen(callListener, PhoneStateListener.LISTEN_NONE);
}
setOutgoingMessageAlarm();
startService(new Intent(this, ForegroundService.class));
@ -259,6 +319,13 @@ public final class App extends Application {
+ getOutgoingMessageLimit() + " in 1 hour reached");
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;
}
@ -338,13 +405,14 @@ public final class App extends Application {
sendOrderedBroadcast(
new Intent(App.QUERY_EXPANSION_PACKS_INTENT),
"android.permission.SEND_SMS",
null,
new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent resultIntent) {
setExpansionPacks(this.getResultExtras(false)
.getStringArrayList(App.QUERY_EXPANSION_PACKS_EXTRA_PACKAGES));
Bundle extras = this.getResultExtras(false);
setExpansionPacks(extras.getStringArrayList(App.QUERY_EXPANSION_PACKS_EXTRA_PACKAGES));
}
},
@ -375,7 +443,7 @@ public final class App extends Application {
String serverUrl = getServerUrl();
if (serverUrl.length() > 0) {
log("Checking for outgoing messages");
pollActive = true;
pollActive = true;
new PollerTask(this).execute();
} else {
log("Can't check outgoing messages; server URL not set");
@ -402,7 +470,7 @@ public final class App extends Application {
if (isEnabled())
{
if (pollSeconds > 0) {
if (pollSeconds > 0) {
alarm.setRepeating(
AlarmManager.ELAPSED_REALTIME_WAKEUP,
SystemClock.elapsedRealtime(),
@ -411,7 +479,7 @@ public final class App extends Application {
log("Checking for outgoing messages every " + pollSeconds + " sec");
} else {
log("Not checking for outgoing messages.");
}
}
}
}
@ -422,47 +490,64 @@ public final class App extends Application {
return str;
}
}
public boolean callNotificationsEnabled()
{
return tryGetBooleanSetting("call_notifications", false);
}
public String getServerUrl() {
return settings.getString("server_url", "");
}
public String getPhoneNumber() {
return settings.getString("phone_number", "");
}
public int getOutgoingPollSeconds() {
return Integer.parseInt(settings.getString("outgoing_interval", "0"));
}
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()
{
return settings.getBoolean("network_failover", false);
return tryGetBooleanSetting("network_failover", false);
}
public boolean isTestMode()
{
return settings.getBoolean("test_mode", false);
}
return tryGetBooleanSetting("test_mode", false);
}
public boolean getKeepInInbox()
{
return settings.getBoolean("keep_in_inbox", false);
return tryGetBooleanSetting("keep_in_inbox", false);
}
public boolean ignoreShortcodes()
{
return settings.getBoolean("ignore_shortcodes", true);
return tryGetBooleanSetting("ignore_shortcodes", true);
}
public boolean ignoreNonNumeric()
{
return settings.getBoolean("ignore_non_numeric", true);
return tryGetBooleanSetting("ignore_non_numeric", true);
}
public String getPassword() {
@ -644,6 +729,11 @@ public final class App extends Application {
).commit();
}
public synchronized void saveStringSetting(String key, String value)
{
settings.edit().putString(key, value).commit();
}
public synchronized void saveBooleanSetting(String key, boolean value)
{
settings.edit().putBoolean(key, value).commit();
@ -761,21 +851,21 @@ public final class App extends Application {
public synchronized boolean canCheck()
{
long time = SystemClock.elapsedRealtime();
long time = System.currentTimeMillis();
return (time - lastCheckTime >= App.CONNECTIVITY_FAILOVER_INTERVAL);
}
public void setChecked()
{
lastCheckTime = SystemClock.elapsedRealtime();
lastCheckTime = System.currentTimeMillis();
}
}
private Map<Integer,ConnectivityCheckState> connectivityCheckStates
= new HashMap<Integer, ConnectivityCheckState>();
private Thread connectivityThread;
private CheckConnectivityTask checkConnectivityTask;
/*
* Normally we rely on Android to automatically switch between
* mobile data and Wi-Fi, but if the phone is connected to a Wi-Fi router
@ -820,7 +910,7 @@ public final class App extends Application {
return;
}
final int networkType = activeNetwork.getType();
final int networkType = activeNetwork.getType();
ConnectivityCheckState state =
connectivityCheckStates.get(networkType);
@ -832,79 +922,20 @@ public final class App extends Application {
}
if (!state.canCheck()
|| (connectivityThread != null && connectivityThread.isAlive()))
|| (checkConnectivityTask != null && checkConnectivityTask.getStatus() != AsyncTask.Status.FINISHED))
{
return;
}
state.setChecked();
connectivityThread = new Thread() {
@Override
public void run()
{
Uri serverUrl = Uri.parse(getServerUrl());
String hostName = serverUrl.getHost();
Uri serverUrl = Uri.parse(getServerUrl());
String hostName = serverUrl.getHost();
log("Checking connectivity to "+hostName+"...");
try
{
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();
log("Checking connectivity to "+hostName+"...");
checkConnectivityTask = new CheckConnectivityTask(this, hostName, networkType);
checkConnectivityTask.execute();
}
private int activeNetworkType = -1;
@ -933,8 +964,21 @@ public final class App extends Application {
asyncCheckConnectivity();
}
private void onConnectivityRestored()
public boolean hasConnectivityError()
{
return connectivityError;
}
public synchronized void onConnectivityError()
{
connectivityError = true;
asyncCheckConnectivity();
}
public synchronized void onConnectivityRestored()
{
connectivityError = false;
inbox.retryAll();
if (getOutgoingPollSeconds() > 0)
@ -963,5 +1007,5 @@ public final class App extends Application {
public synchronized void addQueuedTask(HttpTask task)
{
queuedTasks.add(task);
}
}
}

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.Method;
import org.envaya.sms.ui.Main;
import org.envaya.sms.ui.LogView;
/*
* Service running in foreground to make sure App instance stays
@ -159,7 +159,7 @@ public class ForegroundService extends Service {
System.currentTimeMillis());
PendingIntent contentIntent = PendingIntent.getActivity(this, 0,
new Intent(this, Main.class), 0);
new Intent(this, LogView.class), 0);
notification.setLatestEventInfo(this,
"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;
import android.content.Intent;
import android.os.SystemClock;
import org.envaya.sms.receiver.IncomingMessageRetry;
import org.envaya.sms.task.ForwarderTask;
import org.apache.http.message.BasicNameValuePair;
public abstract class IncomingMessage extends QueuedMessage {
protected String from;
protected String message = "";
protected long timestamp; // unix timestamp in milliseconds
protected long timeReceived; // SystemClock.elapsedRealtime
private ProcessingState state = ProcessingState.None;
public enum ProcessingState
@ -24,6 +30,18 @@ public abstract class IncomingMessage extends QueuedMessage {
super(app);
this.from = from;
this.timestamp = timestamp;
this.timeReceived = SystemClock.elapsedRealtime();
}
public String getMessageBody()
{
return message;
}
public long getAge()
{
return SystemClock.elapsedRealtime() - timeReceived;
}
public long getTimestamp()
@ -77,5 +95,27 @@ public abstract class IncomingMessage extends QueuedMessage {
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.content.ByteArrayBody;
import org.apache.http.entity.mime.content.ContentBody;
import org.apache.http.message.BasicNameValuePair;
import org.envaya.sms.task.ForwarderTask;
public class IncomingMms extends IncomingMessage {
@ -76,22 +75,13 @@ public class IncomingMms extends IncomingMessage {
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>();
int i = 0;
String message = "";
JSONArray partsMetadata = new JSONArray();
for (MmsPart part : parts)
@ -99,12 +89,7 @@ public class IncomingMms extends IncomingMessage {
String formFieldName = "part" + i;
String text = part.getText();
String contentType = part.getContentType();
String partName = part.getName();
if ("text/plain".equals(contentType))
{
message = text;
}
String partName = part.getName();
ContentBody body;
@ -154,18 +139,33 @@ public class IncomingMms extends IncomingMessage {
i++;
}
ForwarderTask task = new ForwarderTask(this,
new BasicNameValuePair("message", message),
new BasicNameValuePair("message_type", App.MESSAGE_TYPE_MMS),
new BasicNameValuePair("mms_parts", partsMetadata.toString())
);
ForwarderTask task = super.getForwarderTask();
task.addParam("mms_parts", partsMetadata.toString());
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()
{
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 java.security.InvalidParameterException;
import java.util.List;
import org.apache.http.message.BasicNameValuePair;
import org.envaya.sms.task.ForwarderTask;
public class IncomingSms extends IncomingMessage {
protected String message;
// constructor for SMS retrieved from android.provider.Telephony.SMS_RECEIVED intent
public IncomingSms(App app, List<SmsMessage> smsParts) throws InvalidParameterException {
super(app,
@ -42,12 +39,7 @@ public class IncomingSms extends IncomingMessage {
public IncomingSms(App app, String from, String message, long timestampMillis) {
super(app, from, timestampMillis);
this.message = message;
}
public String getMessageBody()
{
return message;
}
}
public String getDisplayType()
{
@ -62,18 +54,9 @@ public class IncomingSms extends IncomingMessage {
+ timestamp + "/" +
Uri.encode(message));
}
public void tryForwardToServer() {
if (numRetries > 0)
{
app.log("Retrying forwarding SMS from " + from);
}
new ForwarderTask(this,
new BasicNameValuePair("message_type", App.MESSAGE_TYPE_SMS),
new BasicNameValuePair("message", getMessageBody())
).execute();
}
public String getMessageType()
{
return App.MESSAGE_TYPE_SMS;
}
}

View File

@ -4,11 +4,7 @@ package org.envaya.sms;
import android.content.ContentResolver;
import android.database.Cursor;
import android.net.Uri;
import java.io.File;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.*;
/*
* Utilities for parsing IncomingMms from the MMS content provider tables,
@ -56,9 +52,25 @@ public class MmsUtils
{
long partId = cur.getLong(0);
MmsPart part = new MmsPart(app, partId);
part.setContentType(cur.getString(1));
part.setName(cur.getString(2));
String contentType = cur.getString(1);
if (contentType == null)
{
continue;
}
MmsPart part = new MmsPart(app, partId);
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));
@ -116,9 +128,17 @@ public class MmsUtils
{
long id = c.getLong(0);
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,
getSenderNumber(id),
from,
date * 1000, // MMS timestamp is in seconds for some reason,
// while everything else is in ms
id);

View File

@ -6,8 +6,7 @@ import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.telephony.SmsManager;
import java.util.ArrayList;
import android.os.SystemClock;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashMap;
@ -19,6 +18,7 @@ import java.util.Queue;
import java.util.Set;
import org.apache.http.message.BasicNameValuePair;
import org.envaya.sms.receiver.DequeueOutgoingMessageReceiver;
import org.envaya.sms.receiver.OutgoingMessagePoller;
import org.envaya.sms.task.HttpTask;
public class Outbox {
@ -39,7 +39,7 @@ public class Outbox {
// cache of next time we can send the first message in queue without
// exceeding android sending limit
private long nextValidOutgoingTime;
// enqueue outgoing messages in descending order by priority, ascending by local id
// (order in which message was received)
private PriorityQueue<OutgoingMessage> outgoingQueue = new PriorityQueue<OutgoingMessage>(10,
@ -75,7 +75,10 @@ public class Outbox {
logMessage = "sent successfully";
} else if (status.equals(App.STATUS_FAILED)) {
logMessage = "could not be sent (" + errorMessage + ")";
} else {
} else if (status.equals(App.STATUS_CANCELLED)) {
logMessage = "cancelled";
}
else {
logMessage = "queued";
}
String smsDesc = sms.getLogName();
@ -116,6 +119,8 @@ public class Outbox {
{
sms.setProcessingState(OutgoingMessage.ProcessingState.Sent);
sms.clearSendTimeout();
notifyMessageStatus(sms, App.STATUS_SENT, "");
Uri uri = sms.getUri();
@ -146,9 +151,11 @@ public class Outbox {
}
}
}
public synchronized void messageFailed(OutgoingMessage sms, String error)
{
sms.clearSendTimeout();
if (sms.scheduleRetry())
{
sms.setProcessingState(OutgoingMessage.ProcessingState.Scheduled);
@ -164,49 +171,33 @@ public class Outbox {
maybeDequeueMessage();
}
public synchronized void sendMessage(OutgoingMessage sms) {
String to = sms.getTo();
if (to == null || to.length() == 0)
public synchronized void sendMessage(OutgoingMessage message) {
try
{
notifyMessageStatus(sms, App.STATUS_FAILED,
"Destination address is empty");
return;
}
if (!app.isForwardablePhoneNumber(to))
message.validate();
}
catch (ValidationException ex)
{
// 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;
notifyMessageStatus(message, App.STATUS_FAILED, ex.getMessage());
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();
Uri uri = message.getUri();
if (outgoingMessages.containsKey(uri)) {
app.debug("Duplicate outgoing " + sms.getLogName() + ", skipping");
app.debug("Duplicate outgoing " + message.getLogName() + ", skipping");
return;
}
if (recentSentMessageUris.contains(uri))
{
app.debug("Outgoing " + sms.getLogName() + " already sent, re-notifying server");
notifyMessageStatus(sms, App.STATUS_SENT, "");
app.debug("Outgoing " + message.getLogName() + " already sent, re-notifying server");
notifyMessageStatus(message, App.STATUS_SENT, "");
return;
}
outgoingMessages.put(uri, sms);
enqueueMessage(sms);
outgoingMessages.put(uri, message);
enqueueMessage(message);
}
public synchronized void deleteMessage(OutgoingMessage message)
@ -222,7 +213,7 @@ public class Outbox {
numSendingOutgoingMessages--;
}
notifyMessageStatus(message, App.STATUS_FAILED,
notifyMessageStatus(message, App.STATUS_CANCELLED,
"deleted by user");
app.log(message.getDescription() + " deleted");
notifyChanged();
@ -233,46 +224,32 @@ public class Outbox {
long now = System.currentTimeMillis();
if (nextValidOutgoingTime <= now && numSendingOutgoingMessages < 2)
{
OutgoingMessage sms = outgoingQueue.peek();
OutgoingMessage message = outgoingQueue.peek();
if (sms == null)
if (message == null)
{
return;
}
SmsManager smgr = SmsManager.getDefault();
ArrayList<String> bodyParts = smgr.divideMessage(sms.getMessageBody());
OutgoingMessage.ScheduleInfo schedule = message.scheduleSend();
int numParts = bodyParts.size();
if (numParts > App.OUTGOING_SMS_MAX_COUNT)
if (!schedule.now)
{
outgoingQueue.poll();
outgoingMessages.remove(sms.getUri());
notifyMessageStatus(sms, App.STATUS_FAILED,
"Message has too many parts ("+(numParts)+")");
return;
}
nextValidOutgoingTime = schedule.time;
String packageName = app.chooseOutgoingSmsPackage(numParts);
if (packageName == null)
{
nextValidOutgoingTime = app.getNextValidOutgoingTime(numParts);
if (nextValidOutgoingTime <= now) // should never happen
{
nextValidOutgoingTime = now + 2000;
}
long diff = nextValidOutgoingTime - now;
app.log("Waiting for " + (diff/1000) + " seconds");
AlarmManager alarm = (AlarmManager) app.getSystemService(Context.ALARM_SERVICE);
Intent intent = new Intent(app, DequeueOutgoingMessageReceiver.class);
PendingIntent pendingIntent = PendingIntent.getBroadcast(app,
0,
intent,
@ -282,16 +259,18 @@ public class Outbox {
AlarmManager.RTC_WAKEUP,
nextValidOutgoingTime,
pendingIntent);
return;
}
outgoingQueue.poll();
numSendingOutgoingMessages++;
message.setProcessingState(OutgoingMessage.ProcessingState.Sending);
message.send(schedule);
sms.setProcessingState(OutgoingMessage.ProcessingState.Sending);
message.setSendTimeout();
sms.trySend(bodyParts, packageName);
notifyChanged();
}
}

View File

@ -1,12 +1,16 @@
package org.envaya.sms;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.content.Context;
import org.envaya.sms.receiver.OutgoingMessageRetry;
import android.content.Intent;
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 message;
@ -18,6 +22,12 @@ public class OutgoingMessage extends QueuedMessage {
private ProcessingState state = ProcessingState.None;
public class ScheduleInfo
{
public boolean now = false;
public long time = 0;
}
public enum ProcessingState
{
None, // not doing anything with this sms now... just sitting around
@ -43,6 +53,13 @@ public class OutgoingMessage extends QueuedMessage {
this.state = status;
}
public boolean isCancelable()
{
return this.state == ProcessingState.None
|| this.state == ProcessingState.Queued
|| this.state == ProcessingState.Scheduled;
}
static synchronized int getNextLocalId()
{
return nextLocalId++;
@ -59,10 +76,10 @@ public class OutgoingMessage extends QueuedMessage {
("_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()
{
@ -112,32 +129,7 @@ public class OutgoingMessage extends QueuedMessage {
public int getPriority()
{
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() {
Intent intent = new Intent(app, OutgoingMessageRetry.class);
@ -165,8 +157,42 @@ public class OutgoingMessage extends QueuedMessage {
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();
String action = intent.getAction();
String packageName = intent.getData().getSchemeSpecificPart();
if (packageName != null && packageName.startsWith(context.getPackageName() + ".pack"))
if (packageName != null)
{
app.updateExpansionPacks();
if (packageName.startsWith(context.getPackageName() + ".pack"))
{
app.updateExpansionPacks();
}
}
}
}

View File

@ -35,8 +35,8 @@ public class MessageStatusNotifier extends BroadcastReceiver {
{
// TODO: process message status for parts other than the first one
return;
}
}
int resultCode = getResultCode();
/*

View File

@ -1,15 +1,15 @@
package org.envaya.sms.receiver;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import org.envaya.sms.App;
public class OutgoingMessagePoller extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
App app = (App) context.getApplicationContext();
app.checkOutgoingMessages();
}
}
package org.envaya.sms.receiver;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import org.envaya.sms.App;
public class OutgoingMessagePoller extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
App app = (App) context.getApplicationContext();
app.checkOutgoingMessages();
}
}

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.message.BasicNameValuePair;
import org.envaya.sms.App;
import org.envaya.sms.IncomingMessage;
import org.envaya.sms.OutgoingMessage;
@ -12,11 +11,13 @@ public class ForwarderTask extends HttpTask {
public ForwarderTask(IncomingMessage message, BasicNameValuePair... paramsArr) {
super(message.app, paramsArr);
this.message = message;
params.add(new BasicNameValuePair("action", App.ACTION_INCOMING));
params.add(new BasicNameValuePair("from", message.getFrom()));
params.add(new BasicNameValuePair("timestamp", "" + message.getTimestamp()));
this.message = message;
}
@Override
public boolean isValidContentType(String contentType)
{
return contentType.startsWith("text/xml");
}
@Override
@ -31,10 +32,12 @@ public class ForwarderTask extends HttpTask {
app.outbox.sendMessage(reply);
}
app.inbox.messageForwarded(message);
super.handleResponse(response);
}
@Override
protected void handleFailure() {
protected void handleFailure() {
app.inbox.messageFailed(message);
}
}

View File

@ -4,79 +4,57 @@
*/
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.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.SocketTimeoutException;
import java.net.UnknownHostException;
import java.nio.charset.Charset;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
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;
import org.envaya.sms.App;
import org.envaya.sms.Base64Coder;
import org.envaya.sms.OutgoingMessage;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
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 boolean retryOnConnectivityError;
private BasicNameValuePair[] ctorParams;
public HttpTask(App app, BasicNameValuePair... paramsArr)
{
super();
this.app = app;
this.paramsArr = paramsArr;
params = new ArrayList<BasicNameValuePair>(Arrays.asList(paramsArr));
super(app, app.getServerUrl(), paramsArr);
this.ctorParams = paramsArr;
}
public void setRetryOnConnectivityError(boolean retry)
{
this.retryOnConnectivityError = retry;
this.retryOnConnectivityError = retry; // doesn't work with addParam!
}
protected HttpTask getCopy()
{
return new HttpTask(app, paramsArr);
}
public void setFormParts(List<FormBodyPart> formParts)
{
useMultipartPost = true;
this.formParts = formParts;
}
return new HttpTask(app, ctorParams); // doesn't work with addParam!
}
private String getSignature()
throws NoSuchAlgorithmException, UnsupportedEncodingException
@ -110,6 +88,7 @@ public class HttpTask extends AsyncTask<String, Void, HttpResponse> {
return new String(Base64Coder.encode(digest));
}
@Override
protected HttpResponse doInBackground(String... ignored) {
url = app.getServerUrl();
@ -122,181 +101,136 @@ public class HttpTask extends AsyncTask<String, Void, HttpResponse> {
params.add(new BasicNameValuePair("version", "" + app.getPackageInfo().versionCode));
params.add(new BasicNameValuePair("phone_number", app.getPhoneNumber()));
params.add(new BasicNameValuePair("log", logEntries));
post = new HttpPost(url);
params.add(new BasicNameValuePair("send_limit", "" + app.getOutgoingMessageLimit()));
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)
{
entity.addPart(formPart);
}
post.setEntity(entity);
}
else
{
post.setEntity(new UrlEncodedFormEntity(params, "UTF-8"));
}
HttpClient client = app.getHttpClient();
String signature = getSignature();
post.setHeader("X-Request-Signature", signature);
post.setHeader("User-Agent", "EnvayaSMS/" + app.getPackageInfo().versionName);
HttpResponse response = client.execute(post);
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)
ConnectivityManager cm =
(ConnectivityManager)app.getSystemService(App.CONNECTIVITY_SERVICE);
NetworkInfo activeNetwork = cm.getActiveNetworkInfo();
if (activeNetwork != null)
{
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;
params.add(new BasicNameValuePair("network", "" + activeNetwork.getTypeName()));
}
catch (Throwable ex)
{
post.abort();
app.ungetNewLogEntries(logEntries);
app.logError("Unexpected error while contacting server", ex, true);
return null;
}
params.add(new BasicNameValuePair("log", logEntries));
return super.doInBackground();
}
@Override
protected HttpPost makeHttpPost()
throws Exception
{
HttpPost httpPost = super.makeHttpPost();
String signature = getSignature();
httpPost.setHeader("X-Request-Signature", signature);
return httpPost;
}
protected String getDefaultToAddress()
{
return "";
}
protected List<OutgoingMessage> parseResponseXML(HttpResponse response)
throws IOException, ParserConfigurationException, SAXException
{
List<OutgoingMessage> messages = new ArrayList<OutgoingMessage>();
InputStream responseStream = response.getEntity().getContent();
DocumentBuilder xmlBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
Document xml = xmlBuilder.parse(responseStream);
Document xml = XmlUtils.parseResponse(response);
NodeList smsNodes = xml.getElementsByTagName("sms");
for (int i = 0; i < smsNodes.getLength(); i++) {
Element smsElement = (Element) smsNodes.item(i);
OutgoingMessage sms = new OutgoingMessage(app);
sms.setFrom(app.getPhoneNumber());
String to = smsElement.getAttribute("to");
sms.setTo(to.equals("") ? getDefaultToAddress() : to);
String serverId = smsElement.getAttribute("id");
sms.setServerId(serverId.equals("") ? null : serverId);
String priorityStr = smsElement.getAttribute("priority");
if (!priorityStr.equals(""))
Element messagesElement = (Element) xml.getElementsByTagName("messages").item(0);
if (messagesElement != null)
{
NodeList messageNodes = messagesElement.getChildNodes();
int numNodes = messageNodes.getLength();
for (int i = 0; i < numNodes; i++)
{
try
{
sms.setPriority(Integer.parseInt(priorityStr));
}
catch (NumberFormatException ex)
{
app.log("Invalid message priority: " + priorityStr);
}
}
Element messageElement = (Element) messageNodes.item(i);
StringBuilder messageBody = new StringBuilder();
NodeList childNodes = smsElement.getChildNodes();
int numChildren = childNodes.getLength();
for (int j = 0; j < numChildren; j++)
{
messageBody.append(childNodes.item(j).getNodeValue());
OutgoingMessage message = new OutgoingSms(app);
message.setFrom(app.getPhoneNumber());
String to = messageElement.getAttribute("to");
message.setTo(to.equals("") ? getDefaultToAddress() : to);
String serverId = messageElement.getAttribute("id");
message.setServerId(serverId.equals("") ? null : serverId);
String priorityStr = messageElement.getAttribute("priority");
if (!priorityStr.equals(""))
{
try
{
message.setPriority(Integer.parseInt(priorityStr));
}
catch (NumberFormatException ex)
{
app.log("Invalid message priority: " + priorityStr);
}
}
message.setMessageBody(XmlUtils.getElementText(messageElement));
messages.add(message);
}
sms.setMessageBody(messageBody.toString());
messages.add(sms);
}
return messages;
}
@Override
protected void onPostExecute(HttpResponse response) {
if (response != null)
protected void handleFailure()
{
app.ungetNewLogEntries(logEntries);
}
@Override
protected void handleResponseException(Throwable ex)
{
app.logError("Error in server response", ex);
}
@Override
protected void handleRequestException(Throwable ex)
{
if (ex instanceof IOException)
{
try
{
handleResponse(response);
}
catch (Throwable ex)
{
post.abort();
app.logError("Error processing server response", ex);
handleFailure();
}
try
{
response.getEntity().consumeContent();
}
catch (IOException ex)
{
app.logError("Error while contacting server", ex);
if (ex instanceof UnknownHostException || ex instanceof SocketTimeoutException)
{
if (retryOnConnectivityError)
{
app.addQueuedTask(getCopy());
}
app.onConnectivityError();
}
}
else
{
handleFailure();
}
}
protected void handleResponse(HttpResponse response) throws Exception
{
app.logError("Unexpected error while contacting server", ex, true);
}
}
protected void handleFailure()
@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
{
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));
}
@Override
public boolean isValidContentType(String contentType)
{
return contentType.startsWith("text/xml");
}
@Override
protected void onPostExecute(HttpResponse response) {
super.onPostExecute(response);

View File

@ -1,14 +1,22 @@
package org.envaya.sms.ui;
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.preference.PreferenceManager;
import android.text.Html;
import android.view.View;
import android.widget.TextView;
import org.envaya.sms.App;
import org.envaya.sms.R;
public class Help extends Activity {
private App app;
@Override
public void onCreate(Bundle icicle) {
super.onCreate(icicle);
@ -17,19 +25,44 @@ public class Help extends Activity {
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 />"
+ "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 />";
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

@ -1,157 +1,156 @@
package org.envaya.sms.ui;
import org.envaya.sms.task.HttpTask;
import android.app.Activity;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.text.method.ScrollingMovementMethod;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.widget.ScrollView;
import android.widget.TextView;
import org.apache.http.HttpResponse;
import org.apache.http.message.BasicNameValuePair;
import org.envaya.sms.App;
import org.envaya.sms.R;
public class Main extends Activity {
private App app;
private BroadcastReceiver logReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
updateLogView();
}
};
private ScrollView scrollView;
private TextView info;
private class TestTask extends HttpTask
{
public TestTask() {
super(Main.this.app, new BasicNameValuePair("action", App.ACTION_TEST));
}
@Override
protected void handleResponse(HttpResponse response) throws Exception
{
parseResponseXML(response);
app.log("Server connection OK!");
}
}
private int lastLogEpoch = -1;
public void updateLogView()
{
int logEpoch = app.getLogEpoch();
CharSequence displayedLog = app.getDisplayedLog();
if (lastLogEpoch == logEpoch)
{
int beforeLen = info.getText().length();
int afterLen = displayedLog.length();
if (beforeLen == afterLen)
{
return;
}
info.append(displayedLog, beforeLen, afterLen);
}
else
{
info.setText(displayedLog);
lastLogEpoch = logEpoch;
}
scrollView.post(new Runnable() { public void run() {
scrollView.fullScroll(View.FOCUS_DOWN);
} });
}
/** Called when the activity is first created. */
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
app = (App) getApplication();
setContentView(R.layout.main);
PreferenceManager.setDefaultValues(this, R.xml.prefs, false);
scrollView = (ScrollView) this.findViewById(R.id.info_scroll);
info = (TextView) this.findViewById(R.id.info);
info.setMovementMethod(new ScrollingMovementMethod());
updateLogView();
IntentFilter logReceiverFilter = new IntentFilter();
logReceiverFilter.addAction(App.LOG_CHANGED_INTENT);
registerReceiver(logReceiver, logReceiverFilter);
}
@Override
public void onDestroy()
{
this.unregisterReceiver(logReceiver);
super.onDestroy();
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
// Handle item selection
switch (item.getItemId()) {
case R.id.settings:
startActivity(new Intent(this, Prefs.class));
return true;
case R.id.check_now:
app.checkOutgoingMessages();
return true;
case R.id.retry_now:
app.retryStuckMessages();
return true;
case R.id.forward_inbox:
startActivity(new Intent(this, MessagingInbox.class));
return true;
case R.id.pending:
startActivity(new Intent(this, PendingMessages.class));
return true;
case R.id.test:
app.log("Testing server connection...");
new TestTask().execute();
return true;
default:
return super.onOptionsItemSelected(item);
}
}
// first time the Menu key is pressed
@Override
public boolean onCreateOptionsMenu(Menu menu) {
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.mainmenu, menu);
return(true);
}
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
MenuItem retryItem = menu.findItem(R.id.retry_now);
int pendingTasks = app.getPendingTaskCount();
retryItem.setEnabled(pendingTasks > 0);
retryItem.setTitle("Retry All (" + pendingTasks + ")");
return true;
}
package org.envaya.sms.ui;
import org.envaya.sms.task.HttpTask;
import android.app.Activity;
import android.content.*;
import android.net.Uri;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.text.method.LinkMovementMethod;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.widget.ScrollView;
import android.widget.TextView;
import org.apache.http.HttpResponse;
import org.apache.http.message.BasicNameValuePair;
import org.envaya.sms.App;
import org.envaya.sms.R;
public class LogView extends Activity {
private App app;
private BroadcastReceiver logReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
updateLogView();
}
};
private ScrollView scrollView;
private TextView info;
private class TestTask extends HttpTask
{
public TestTask() {
super(LogView.this.app, new BasicNameValuePair("action", App.ACTION_TEST));
}
@Override
protected void handleResponse(HttpResponse response) throws Exception
{
app.log("Server connection OK!");
}
}
private int lastLogEpoch = -1;
public void updateLogView()
{
int logEpoch = app.getLogEpoch();
CharSequence displayedLog = app.getDisplayedLog();
if (lastLogEpoch == logEpoch)
{
int beforeLen = info.getText().length();
int afterLen = displayedLog.length();
if (beforeLen == afterLen)
{
return;
}
info.append(displayedLog, beforeLen, afterLen);
}
else
{
info.setText(displayedLog);
lastLogEpoch = logEpoch;
}
scrollView.post(new Runnable() { public void run() {
scrollView.fullScroll(View.FOCUS_DOWN);
} });
}
/** Called when the activity is first created. */
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
app = (App) getApplication();
setContentView(R.layout.log_view);
PreferenceManager.setDefaultValues(this, R.xml.prefs, false);
scrollView = (ScrollView) this.findViewById(R.id.info_scroll);
info = (TextView) this.findViewById(R.id.info);
info.setMovementMethod(LinkMovementMethod.getInstance());
//info.setMovementMethod(new ScrollingMovementMethod());
updateLogView();
IntentFilter logReceiverFilter = new IntentFilter();
logReceiverFilter.addAction(App.LOG_CHANGED_INTENT);
registerReceiver(logReceiver, logReceiverFilter);
}
@Override
public void onDestroy()
{
this.unregisterReceiver(logReceiver);
super.onDestroy();
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
// Handle item selection
switch (item.getItemId()) {
case R.id.settings:
startActivity(new Intent(this, Prefs.class));
return true;
case R.id.check_now:
app.checkOutgoingMessages();
return true;
case R.id.retry_now:
app.retryStuckMessages();
return true;
case R.id.forward_inbox:
startActivity(new Intent(this, MessagingInbox.class));
return true;
case R.id.pending:
startActivity(new Intent(this, PendingMessages.class));
return true;
case R.id.test:
app.log("Testing server connection...");
new TestTask().execute();
return true;
default:
return super.onOptionsItemSelected(item);
}
}
// first time the Menu key is pressed
@Override
public boolean onCreateOptionsMenu(Menu menu) {
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.mainmenu, menu);
return(true);
}
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
MenuItem retryItem = menu.findItem(R.id.retry_now);
int pendingTasks = app.getPendingTaskCount();
retryItem.setEnabled(pendingTasks > 0);
retryItem.setTitle("Retry All (" + pendingTasks + ")");
return true;
}
}

View File

@ -89,6 +89,10 @@ public class Prefs extends PreferenceActivity implements OnSharedPreferenceChang
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"))
{
app.log("Phone number changed to: " + app.getDisplayString(app.getPhoneNumber()));