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:
parent
d93ff76231
commit
f53ccc3cc9
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
local.properties
|
||||
bin
|
||||
gen
|
||||
nbandroid
|
@ -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
17
ant.properties
Normal 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.
|
||||
|
@ -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
164
build.xml
@ -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>
|
||||
|
@ -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
13
project.properties
Normal 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
0
res/layout/main.xml → res/layout/log_view.xml
Executable file → Normal 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"
|
||||
|
@ -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)
|
||||
{
|
||||
|
2
server/php/example/mms_parts/.gitignore
vendored
Normal file
2
server/php/example/mms_parts/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
*
|
||||
!.gitignore
|
@ -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;
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
31
src/org/envaya/sms/CallListener.java
Normal file
31
src/org/envaya/sms/CallListener.java
Normal 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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",
|
||||
|
49
src/org/envaya/sms/IncomingCall.java
Normal file
49
src/org/envaya/sms/IncomingCall.java
Normal 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);
|
||||
}
|
||||
}
|
@ -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())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
127
src/org/envaya/sms/OutgoingSms.java
Normal file
127
src/org/envaya/sms/OutgoingSms.java
Normal 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+")");
|
||||
}
|
||||
}
|
||||
}
|
9
src/org/envaya/sms/ValidationException.java
Normal file
9
src/org/envaya/sms/ValidationException.java
Normal file
@ -0,0 +1,9 @@
|
||||
package org.envaya.sms;
|
||||
|
||||
public class ValidationException extends Exception {
|
||||
|
||||
public ValidationException(String message)
|
||||
{
|
||||
super(message);
|
||||
}
|
||||
}
|
41
src/org/envaya/sms/XmlUtils.java
Normal file
41
src/org/envaya/sms/XmlUtils.java
Normal 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;
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -35,8 +35,8 @@ public class MessageStatusNotifier extends BroadcastReceiver {
|
||||
{
|
||||
// TODO: process message status for parts other than the first one
|
||||
return;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int resultCode = getResultCode();
|
||||
|
||||
/*
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
29
src/org/envaya/sms/receiver/OutgoingMessageTimeout.java
Normal file
29
src/org/envaya/sms/receiver/OutgoingMessageTimeout.java
Normal 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");
|
||||
}
|
||||
}
|
200
src/org/envaya/sms/task/BaseHttpTask.java
Normal file
200
src/org/envaya/sms/task/BaseHttpTask.java
Normal 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)
|
||||
{
|
||||
}
|
||||
|
||||
}
|
101
src/org/envaya/sms/task/CheckConnectivityTask.java
Normal file
101
src/org/envaya/sms/task/CheckConnectivityTask.java
Normal 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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
311
src/org/envaya/sms/ui/Main.java → src/org/envaya/sms/ui/LogView.java
Executable file → Normal file
311
src/org/envaya/sms/ui/Main.java → src/org/envaya/sms/ui/LogView.java
Executable file → Normal 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;
|
||||
}
|
||||
|
||||
}
|
@ -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()));
|
||||
|
Loading…
Reference in New Issue
Block a user