5
0
mirror of https://github.com/cwinfo/envayasms.git synced 2024-10-18 00:40:42 +00:00

version 3.0 - real-time AMQP connections; change server API format from XML to JSON, update PHP server library; persistent storage of pending messages

This commit is contained in:
Jesse Young 2012-04-04 14:16:26 -07:00
parent f53ccc3cc9
commit 239ee1fd52
71 changed files with 3627 additions and 832 deletions

3
.gitignore vendored
View File

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

3
.gitmodules vendored
View File

@ -1,3 +1,6 @@
[submodule "server/php/example/httpserver"]
path = server/php/example/httpserver
url = git://github.com/youngj/httpserver.git
[submodule "server/php/example/php-amqplib"]
path = server/php/example/php-amqplib
url = git://github.com/youngj/php-amqplib.git

View File

@ -1,8 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.envaya.sms"
android:versionCode="18"
android:versionName="2.0.5">
android:versionCode="29"
android:versionName="3.0.0">
<uses-sdk android:minSdkVersion="4" />
@ -23,52 +23,70 @@
<application android:name="org.envaya.sms.App"
android:icon="@drawable/icon" android:label="@string/app_name">
<activity android:name=".ui.LogView" android:label="@string/app_name">
<activity android:name="org.envaya.sms.ui.Main" android:label="@string/app_name" android:noHistory="true">
<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 android:name="org.envaya.sms.ui.LogView" android:label="@string/log_view_title">
</activity>
<activity android:name=".ui.TestPhoneNumbers" android:label="EnvayaSMS : Test Phone Numbers">
<activity android:name="org.envaya.sms.ui.Help" android:label="@string/help_title">
</activity>
<activity android:name=".ui.IgnoredPhoneNumbers" android:label="EnvayaSMS : Ignored Phone Numbers">
<activity android:name="org.envaya.sms.ui.TestPhoneNumbers" android:label="@string/test_phone_numbers_title">
</activity>
<activity android:name=".ui.MessagingInbox" android:label="EnvayaSMS : Forward Inbox">
<activity android:name="org.envaya.sms.ui.IgnoredPhoneNumbers" android:label="@string/ignored_phone_numbers_title">
</activity>
<activity android:name=".ui.PendingMessages" android:label="EnvayaSMS : Pending Messages">
<activity android:name="org.envaya.sms.ui.MessagingSmsInbox" android:label="@string/forward_saved_title">
</activity>
<activity android:name=".ui.Prefs" android:label="EnvayaSMS : Settings">
<activity android:name="org.envaya.sms.ui.MessagingMmsInbox" android:label="@string/forward_saved_title">
</activity>
<receiver android:name=".receiver.SmsReceiver">
<activity android:name="org.envaya.sms.ui.MessagingSentSms" android:label="@string/forward_saved_title">
</activity>
<activity android:name="org.envaya.sms.ui.PendingMessages" android:label="@string/pending_messages_title">
</activity>
<activity android:name="org.envaya.sms.ui.Prefs" android:label="@string/settings_title">
</activity>
<activity android:name="org.envaya.sms.ui.ExpansionPacks" android:label="...">
</activity>
<receiver android:name="org.envaya.sms.receiver.SmsReceiver">
<intent-filter android:priority="101">
<action android:name="android.provider.Telephony.SMS_RECEIVED" />
</intent-filter>
</receiver>
<receiver android:name=".receiver.OutgoingSmsReceiver">
<receiver android:name="org.envaya.sms.receiver.OutgoingSmsReceiver">
<intent-filter>
<action android:name="org.envaya.sms.OUTGOING_SMS" />
<data android:scheme="content" />
</intent-filter>
</receiver>
<receiver android:name=".receiver.MessageStatusNotifier" android:exported="true">
<receiver android:name="org.envaya.sms.receiver.MessageStatusNotifier" android:exported="true">
<intent-filter>
<action android:name="org.envaya.sms.MESSAGE_STATUS" />
<data android:scheme="content" />
</intent-filter>
</receiver>
<receiver android:name="org.envaya.sms.receiver.NudgeReceiver" android:exported="true">
<intent-filter>
<action android:name="org.envaya.sms.NUDGE" />
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
<!--
we don't really use message delivery notifications yet...
@ -80,31 +98,28 @@
</receiver>
-->
<receiver android:name=".receiver.DequeueOutgoingMessageReceiver">
<receiver android:name="org.envaya.sms.receiver.DequeueOutgoingMessageReceiver">
</receiver>
<receiver android:name=".receiver.OutgoingMessageTimeout">
<receiver android:name="org.envaya.sms.receiver.OutgoingMessageTimeout">
</receiver>
<receiver android:name=".receiver.OutgoingMessagePoller">
<receiver android:name="org.envaya.sms.receiver.OutgoingMessagePoller">
</receiver>
<receiver android:name=".receiver.OutgoingMessageRetry">
<receiver android:name="org.envaya.sms.receiver.OutgoingMessageRetry">
</receiver>
<receiver android:name=".receiver.IncomingMessageRetry">
<receiver android:name="org.envaya.sms.receiver.IncomingMessageRetry">
</receiver>
<receiver android:name=".receiver.ReenableWifiReceiver">
<receiver android:name="org.envaya.sms.receiver.ReenableWifiReceiver">
</receiver>
<receiver android:name=".receiver.BootReceiver">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
<receiver android:name="org.envaya.sms.receiver.StartAmqpConsumer">
</receiver>
<receiver android:name=".receiver.ExpansionPackInstallReceiver">
<receiver android:name="org.envaya.sms.receiver.ExpansionPackInstallReceiver">
<intent-filter>
<action android:name="android.intent.action.PACKAGE_ADDED" />
<action android:name="android.intent.action.PACKAGE_REMOVED" />
@ -113,13 +128,13 @@
</intent-filter>
</receiver>
<receiver android:name=".receiver.ConnectivityChangeReceiver" >
<receiver android:name="org.envaya.sms.receiver.ConnectivityChangeReceiver" >
<intent-filter>
<action android:name="android.net.conn.CONNECTIVITY_CHANGE" />
</intent-filter>
</receiver>
<receiver android:name=".receiver.DeviceStatusReceiver">
<receiver android:name="org.envaya.sms.receiver.DeviceStatusReceiver">
<intent-filter>
<action android:name="android.intent.action.ACTION_POWER_CONNECTED" />
<action android:name="android.intent.action.ACTION_POWER_DISCONNECTED" />
@ -128,11 +143,19 @@
</intent-filter>
</receiver>
<service android:name=".CheckMmsInboxService">
<service android:name="org.envaya.sms.service.CheckMessagingService">
</service>
<service android:name=".ForegroundService">
<service android:name="org.envaya.sms.service.EnabledChangedService">
</service>
<service android:name="org.envaya.sms.service.ForegroundService">
</service>
<service android:name="org.envaya.sms.service.AmqpConsumerService">
</service>
<service android:name="org.envaya.sms.service.AmqpHeartbeatService">
</service>
</application>
</manifest>

View File

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

View File

@ -45,21 +45,20 @@
unless="sdk.dir"
/>
<!--
Import per project custom build rules if present at the root of the project.
This is the place to put custom intermediary targets such as:
-pre-build
-pre-compile
-post-compile (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})
-post-package
-post-build
-pre-clean
-->
<!-- 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 file="custom_rules.xml" optional="true" />
<!-- Import the actual build file.

View File

@ -0,0 +1,11 @@
# 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

15
export.properties Normal file
View File

@ -0,0 +1,15 @@
# Export properties
#
# This file must be checked in Version Control Systems.
# The main content for this file is:
# - package name for the application being export
# - list of the projects being export
# - version code for the application
# 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 alias to use.
# The password will be asked during the build when you use the 'release' target.

BIN
libs/commons-cli-1.1.jar Normal file

Binary file not shown.

BIN
libs/commons-io-1.2.jar Normal file

Binary file not shown.

BIN
libs/rabbitmq-client.jar Normal file

Binary file not shown.

20
proguard-project.txt Normal file
View File

@ -0,0 +1,20 @@
# To enable ProGuard in your project, edit project.properties
# to define the proguard.config property as described in that file.
#
# Add project specific ProGuard rules here.
# By default, the flags in this file are appended to flags specified
# in ${sdk.dir}/tools/proguard/proguard-android.txt
# You can edit the include path and order by changing the ProGuard
# include property in project.properties.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# Add any project specific keep options here:
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}

View File

@ -3,18 +3,22 @@
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent" android:background="#333333">
<ScrollView android:layout_width="fill_parent"
android:layout_height="fill_parent" android:layout_weight="1">
<TextView
android:linksClickable="true"
android:drawablePadding="5px"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:layout_height="wrap_content"
android:id="@+id/help"
android:autoLink="web"
android:textColor="#FFFFFF"
android:layout_margin="5px">
</TextView>
</ScrollView>
</TextView>
<Button
android:id="@+id/reset"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="10dp"
android:onClick="resetClicked"
android:text="Reset All Settings" />
</LinearLayout>

View File

@ -5,13 +5,17 @@
android:layout_height="fill_parent"
android:paddingLeft="8dp"
android:paddingRight="8dp">
<Spinner android:id="@+android:id/inbox_selector"
android:layout_width="fill_parent"
android:layout_height="wrap_content">
</Spinner>
<ListView android:id="@android:id/list"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:layout_weight="1"
/>
<TextView android:id="@android:id/empty"
android:text="The inbox is empty."
android:text="No messages found."
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:layout_weight="1"

View File

@ -3,14 +3,46 @@
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent" android:background="#333333">
<ScrollView android:id="@+id/info_scroll" android:layout_width="fill_parent"
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:clickable="true"
android:onClick="infoClicked"
android:padding="10px"
android:background="#666666">
<TextView
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:id="@+id/heading"
android:textSize="15sp"
android:textColor="#FFFFFF"
android:gravity="center"
></TextView>
<TextView
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:textColor="#FFFFFF"
android:gravity="center"
android:id="@+id/info"
></TextView>
</LinearLayout>
<ScrollView android:id="@+id/log_scroll" android:layout_width="fill_parent"
android:layout_height="fill_parent" android:layout_weight="1">
<TextView
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:id="@+id/info"
android:id="@+id/log"
android:textColor="#FFFFFF"
android:textColorLink="#FFFFFF"
android:layout_margin="5px"></TextView>
</ScrollView>
<Button
android:id="@+id/upgrade_button"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:padding="20sp"
android:onClick="upgradeClicked"
android:text="" />
</LinearLayout>

View File

@ -11,6 +11,13 @@
android:text="@string/test_phone_numbers">
</TextView>
<CheckBox android:id="@+id/auto_add_outgoing"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="autoAddOutgoingClicked"
android:text="Automatically add recipients of outgoing messages"
/>
<ListView android:id="@android:id/list"
android:layout_width="fill_parent"
android:layout_height="fill_parent"

View File

@ -9,9 +9,9 @@
<item android:id="@+id/check_now"
android:icon="@drawable/ic_menu_tick"
android:title="@string/check_now" />
<item android:id="@+id/forward_inbox"
<item android:id="@+id/forward_saved"
android:icon="@drawable/ic_menu_dialog"
android:title="@string/forward_inbox" />
android:title="@string/forward_saved" />
<item android:id="@+id/retry_now"
android:icon="@drawable/ic_menu_magnet"
android:title="@string/retry_now" />

View File

@ -1,18 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">EnvayaSMS</string>
<string name="running">EnvayaSMS running</string>
<string name="disabled">EnvayaSMS disabled</string>
<string name="started">EnvayaSMS started.</string>
<string name="stopped">EnvayaSMS stopped.</string>
<string name="log_view_title">EnvayaSMS : Log View</string>
<string name="add_phone_title">EnvayaSMS : Add Phone</string>
<string name="help_title">EnvayaSMS : Help</string>
<string name='test_phone_numbers_title'>EnvayaSMS : Test Phone Numbers</string>
<string name='ignored_phone_numbers_title'>EnvayaSMS : Ignored Phone Numbers</string>
<string name='forward_saved_title'>EnvayaSMS : Forward Saved Messages</string>
<string name='pending_messages_title'>EnvayaSMS : Pending Messages</string>
<string name='settings_title'>EnvayaSMS : Settings</string>
<string name="settings">Settings</string>
<string name="test">Test Connection</string>
<string name="check_now">Check Messages</string>
<string name="help">Help</string>
<string name="pending">Pending Msgs...</string>
<string name="retry_now">Retry</string>
<string name="forward_inbox">Fwd Inbox...</string>
<string name='service_started'>New SMS will be forwarded to server</string>
<string name='test_phone_numbers'>When running EnvayaSMS in Test Mode,
<string name="forward_saved">Fwd Saved...</string>
<string name='service_started'>New messages will be forwarded to server</string>
<string name='test_phone_numbers'>When running Telerivet in Test Mode,
EnvayaSMS will only forward SMS messages from the phone numbers
listed below. (Incoming SMS messages from other phone numbers will be saved
in the normal Messaging inbox, and outgoing messages will be ignored.)</string>
in the normal Messaging inbox.)</string>
<string name='ignored_phone_numbers'>
EnvayaSMS will ignore SMS messages from the phone numbers listed below.
Incoming messages from these senders will be saved in the normal Messaging inbox.</string>

View File

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen
xmlns:android="http://schemas.android.com/apk/res/android">
<PreferenceScreen
android:key="pack01"
android:title="SMS Expansion Pack 1"
android:summary="...">
<intent
android:action="android.intent.action.VIEW"
android:data="market://details?id=org.envaya.sms.pack01"
/>
</PreferenceScreen>
<PreferenceScreen
android:key="pack02"
android:title="SMS Expansion Pack 2"
android:summary="...">
<intent
android:action="android.intent.action.VIEW"
android:data="market://details?id=org.envaya.sms.pack02"
/>
</PreferenceScreen>
</PreferenceScreen>

View File

@ -6,10 +6,12 @@
android:key="enabled"
android:title="Enable EnvayaSMS"
android:defaultValue='false'
android:summaryOn="All new SMS will be forwarded between phone and server"
android:summaryOff="New SMS will not be forwarded between phone and server"
android:summaryOn="New messages will be forwarded between phone and server"
android:summaryOff="New messages will not be forwarded between phone and server"
></CheckBoxPreference>
<PreferenceCategory android:title="Server Settings">
<EditTextPreference
android:key="server_url"
android:title="Server URL"
@ -38,34 +40,40 @@
android:entryValues="@array/check_intervals_values"
></ListPreference>
</PreferenceCategory>
<PreferenceCategory android:title="Messaging Settings">
<CheckBoxPreference
android:key="keep_in_inbox"
android:title="Keep new messages"
android:summaryOff="Incoming SMS will not be stored in Messaging inbox"
android:summaryOn="Incoming SMS will be stored in Messaging inbox"
android:summaryOff="Incoming messages will not be stored in Messaging inbox"
android:summaryOn="Incoming messages 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"
android:summaryOff="Telerivet will not notify server when phone receives an incoming call"
android:summaryOn="Telerivet will notify server when phone receives an incoming call"
></CheckBoxPreference>
<ListPreference
android:key="wifi_sleep_policy"
android:title="Wi-Fi sleep policy"
android:defaultValue="never"
android:entries="@array/wifi_sleep_policies"
android:entryValues="@array/wifi_sleep_policies_values"
<PreferenceScreen
android:key="send_limit"
android:title="SMS rate limit"
android:summary="..."
>
</ListPreference>
<intent
android:action="android.intent.action.MAIN"
android:targetPackage="org.envaya.sms"
android:targetClass="org.envaya.sms.ui.ExpansionPacks" />
</PreferenceScreen>
<CheckBoxPreference
android:key="network_failover"
android:title="Network failover"
android:summaryOff="Do nothing if phone can't connect to server via Wi-Fi"
android:summaryOn="Automatically switch to mobile data if phone can't connect to server via Wi-Fi"
android:key="forward_sent"
android:title="Forward sent messages"
android:summaryOff="SMS sent from Messaging app will not be forwarded to server"
android:summaryOn="SMS sent from Messaging app will be forwarded to server"
></CheckBoxPreference>
<PreferenceScreen
@ -98,14 +106,112 @@
android:targetClass="org.envaya.sms.ui.TestPhoneNumbers" />
</PreferenceScreen>
<PreferenceScreen
android:key="help"
android:title="About EnvayaSMS"
</PreferenceCategory>
<PreferenceCategory android:title="Networking Settings">
<ListPreference
android:key="wifi_sleep_policy"
android:title="Wi-Fi sleep policy"
android:defaultValue="never"
android:entries="@array/wifi_sleep_policies"
android:entryValues="@array/wifi_sleep_policies_values"
>
</ListPreference>
<CheckBoxPreference
android:key="network_failover"
android:title="Network failover"
android:summaryOff="Do nothing if phone can't connect to server via Wi-Fi"
android:summaryOn="Automatically switch to mobile data if phone can't connect to server via Wi-Fi"
></CheckBoxPreference>
</PreferenceCategory>
<PreferenceCategory android:title="AMQP Settings (Real-Time Connection)">
<CheckBoxPreference
android:key="amqp_enabled"
android:title="Enable AMQP"
android:summaryOff="AMQP is disabled"
android:summaryOn="AMQP is enabled"
></CheckBoxPreference>
<EditTextPreference
android:key="amqp_host"
android:title="AMQP Host"
android:inputType="textUri"
android:defaultValue=""
android:dependency="amqp_enabled"
></EditTextPreference>
<EditTextPreference
android:key="amqp_port"
android:title="AMQP Port"
android:inputType="number"
android:defaultValue="5672"
android:dependency="amqp_enabled"
></EditTextPreference>
<EditTextPreference
android:key="amqp_vhost"
android:title="AMQP Virtual Host"
android:inputType="text"
android:defaultValue="/"
android:dependency="amqp_enabled"
></EditTextPreference>
<CheckBoxPreference
android:key="amqp_ssl"
android:title="AMQP SSL"
android:summaryOff="Off (Plain text)"
android:summaryOn="On (Encrypted)"
android:dependency="amqp_enabled"
></CheckBoxPreference>
<EditTextPreference
android:key="amqp_user"
android:title="AMQP User"
android:defaultValue=""
android:dependency="amqp_enabled"
></EditTextPreference>
<EditTextPreference
android:key="amqp_password"
android:title="AMQP Password"
android:defaultValue=""
android:password="true"
android:dependency="amqp_enabled"
></EditTextPreference>
<EditTextPreference
android:key="amqp_queue"
android:title="AMQP Queue Name"
android:defaultValue=""
android:dependency="amqp_enabled"
></EditTextPreference>
<EditTextPreference
android:key="amqp_heartbeat"
android:title="AMQP Heartbeat (sec)"
android:inputType="number"
android:defaultValue="60"
android:dependency="amqp_enabled"
></EditTextPreference>
</PreferenceCategory>
<PreferenceCategory android:title="Help">
<PreferenceScreen
android:key="help"
android:title="About EnvayaSMS"
>
<intent
android:action="android.intent.action.MAIN"
android:targetPackage="org.envaya.sms"
android:targetClass="org.envaya.sms.ui.Help" />
</PreferenceScreen>
</PreferenceCategory>
</PreferenceScreen>

View File

@ -1,24 +1,33 @@
<?php
/*
* PHP server library for EnvayaSMS
* PHP server library for EnvayaSMS 3.0
*
* For example usage see example/www/index.php
* For example usage see example/www/gateway.php
*/
class EnvayaSMS
class EnvayaSMS
{
const ACTION_INCOMING = 'incoming';
const ACTION_OUTGOING = 'outgoing';
const ACTION_FORWARD_SENT = 'forward_sent';
const ACTION_SEND_STATUS = 'send_status';
const ACTION_DEVICE_STATUS = 'device_status';
const ACTION_TEST = 'test';
const ACTION_OUTGOING = 'outgoing';
const ACTION_AMQP_STARTED = 'amqp_started';
// ACTION_OUTGOING should probably be const ACTION_POLL = 'poll',
// but 'outgoing' maintains backwards compatibility between new phone versions with old servers
const STATUS_QUEUED = 'queued';
const STATUS_FAILED = 'failed';
const STATUS_SENT = 'sent';
const STATUS_CANCELLED = 'cancelled';
const EVENT_SEND = 'send';
const EVENT_CANCEL = 'cancel';
const EVENT_CANCEL_ALL = 'cancel_all';
const EVENT_LOG = 'log';
const EVENT_SETTINGS = 'settings';
const DEVICE_STATUS_POWER_CONNECTED = "power_connected";
const DEVICE_STATUS_POWER_DISCONNECTED = "power_disconnected";
const DEVICE_STATUS_BATTERY_LOW = "battery_low";
@ -29,8 +38,10 @@ class EnvayaSMS
const MESSAGE_TYPE_MMS = 'mms';
const MESSAGE_TYPE_CALL = 'call';
const NETWORK_MOBILE = "MOBILE";
const NETWORK_WIFI = "WIFI";
// power source constants same as from Android's BatteryManager.EXTRA_PLUGGED
const POWER_SOURCE_BATTERY = 0;
const POWER_SOURCE_AC = 1;
const POWER_SOURCE_USB = 2;
static function escape($val)
{
@ -41,60 +52,36 @@ class EnvayaSMS
static function get_request()
{
if (!isset(static::$request))
if (!isset(self::$request))
{
$version = @$_POST['version'];
// If API version changes, could return
// different EnvayaSMS_Request instance
// to support multiple phone versions
if (isset($_POST['action']))
{
self::$request = new EnvayaSMS_ActionRequest();
}
else
{
self::$request = new EnvayaSMS_Request();
}
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();
return self::$request;
}
}
class EnvayaSMS_Request
{
private $request_action;
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->network = @$_POST['network'];
$this->version = (int)@$_POST['version'];
if (preg_match('#/(?P<version_name>[\w\.\-]+) \(Android; SDK (?P<sdk_int>\d+); (?P<manufacturer>[^;]*); (?P<model>[^\)]*)\)#',
@$_SERVER['HTTP_USER_AGENT'], $matches))
@ -106,6 +93,112 @@ class EnvayaSMS_Request
}
}
function supports_json()
{
return $this->version >= 28;
}
function supports_update_settings()
{
return $this->version >= 29;
}
function get_response_type()
{
if ($this->supports_json())
{
return 'application/json';
}
else
{
return 'text/xml';
}
}
function render_response($events = null /* optional array of EnvayaSMS_Event objects */)
{
if ($this->supports_json())
{
return json_encode(array('events' => $events));
}
else
{
ob_start();
echo "<?xml version='1.0' encoding='UTF-8'?>\n";
echo "<response>";
if ($events)
{
foreach ($events as $event)
{
echo "<messages>";
if ($event instanceof EnvayaSMS_Event_Send)
{
if ($event->messages)
{
foreach ($event->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 "<$type$id$to$priority>".EnvayaSMS::escape($message->message)."</$type>";
}
}
}
echo "</messages>";
}
}
echo "</response>";
return ob_get_clean();
}
}
function render_error_response($message)
{
if ($this->supports_json())
{
return json_encode(array('error' => array('message' => $message)));
}
else
{
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();
}
}
}
class EnvayaSMS_ActionRequest extends EnvayaSMS_Request
{
private $request_action;
public $settings_version; // integer version of current settings (as provided by server)
public $phone_number; // phone number of Android phone
public $log; // app log messages since last successful request
public $now; // current time (ms since Unix epoch) according to Android clock
public $network; // name of network, like WIFI or MOBILE (may vary depending on phone)
public $battery; // battery level as percentage
public $power; // power source integer, see EnvayaSMS::POWER_SOURCE_*
function __construct()
{
parent::__construct();
$this->phone_number = $_POST['phone_number'];
$this->log = $_POST['log'];
$this->network = @$_POST['network'];
$this->now = @$_POST['now'];
$this->settings_version = @$_POST['settings_version'];
$this->battery = @$_POST['battery'];
$this->power = @$_POST['power'];
}
function get_action()
{
if (!$this->request_action)
@ -121,6 +214,8 @@ class EnvayaSMS_Request
{
case EnvayaSMS::ACTION_INCOMING:
return new EnvayaSMS_Action_Incoming($this);
case EnvayaSMS::ACTION_FORWARD_SENT:
return new EnvayaSMS_Action_ForwardSent($this);
case EnvayaSMS::ACTION_OUTGOING:
return new EnvayaSMS_Action_Outgoing($this);
case EnvayaSMS::ACTION_SEND_STATUS:
@ -129,6 +224,8 @@ class EnvayaSMS_Request
return new EnvayaSMS_Action_Test($this);
case EnvayaSMS::ACTION_DEVICE_STATUS:
return new EnvayaSMS_Action_DeviceStatus($this);
case EnvayaSMS::ACTION_AMQP_STARTED:
return new EnvayaSMS_Action_AmqpStarted($this);
default:
return new EnvayaSMS_Action($this);
}
@ -163,29 +260,8 @@ class EnvayaSMS_Request
$input .= ",$password";
//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 "<$type$id$to$priority>".EnvayaSMS::escape($message->message)."</$type>";
}
echo "</messages>";
echo "</response>";
return ob_get_clean();
}
}
class EnvayaSMS_OutgoingMessage
@ -197,6 +273,10 @@ class EnvayaSMS_OutgoingMessage
public $type; // EnvayaSMS::MESSAGE_TYPE_* value (default sms)
}
/*
* An 'action' is the term for a HTTP request that app sends to the server.
*/
class EnvayaSMS_Action
{
public $type;
@ -233,24 +313,19 @@ class EnvayaSMS_MMS_Part
}
}
class EnvayaSMS_Action_Incoming extends EnvayaSMS_Action
abstract class EnvayaSMS_Action_Forward extends EnvayaSMS_Action
{
public $from; // Sender phone number
public $message; // The message body of the SMS, or the content of the text/plain part of the MMS.
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_type = $_POST['message_type'];
$this->timestamp = @$_POST['timestamp'];
$this->age = @$_POST['age'];
if ($this->message_type == EnvayaSMS::MESSAGE_TYPE_MMS)
{
@ -261,10 +336,41 @@ class EnvayaSMS_Action_Incoming extends EnvayaSMS_Action
}
}
}
}
function get_response_xml($messages)
class EnvayaSMS_Action_Incoming extends EnvayaSMS_Action_Forward
{
public $from; // Sender phone number
function __construct($request)
{
return $this->request->get_messages_xml($messages);
parent::__construct($request);
$this->type = EnvayaSMS::ACTION_INCOMING;
$this->from = $_POST['from'];
}
}
class EnvayaSMS_Action_ForwardSent extends EnvayaSMS_Action_Forward
{
public $to; // Recipient phone number
function __construct($request)
{
parent::__construct($request);
$this->type = EnvayaSMS::ACTION_FORWARD_SENT;
$this->to = $_POST['to'];
}
}
class EnvayaSMS_Action_AmqpStarted extends EnvayaSMS_Action
{
public $consumer_tag;
function __construct($request)
{
parent::__construct($request);
$this->type = EnvayaSMS::ACTION_AMQP_STARTED;
$this->consumer_tag = $_POST['consumer_tag'];
}
}
@ -275,11 +381,6 @@ class EnvayaSMS_Action_Outgoing extends EnvayaSMS_Action
parent::__construct($request);
$this->type = EnvayaSMS::ACTION_OUTGOING;
}
function get_response_xml($messages)
{
return $this->request->get_messages_xml($messages);
}
}
class EnvayaSMS_Action_Test extends EnvayaSMS_Action
@ -318,3 +419,89 @@ class EnvayaSMS_Action_DeviceStatus extends EnvayaSMS_Action
$this->status = $_POST['status'];
}
}
/*
* An 'event' is the term for something the server sends to the app,
* either via a response to an 'action', or directly via AMQP.
*/
class EnvayaSMS_Event
{
public $event;
/*
* Formats this event as the body of an AMQP message.
*/
function render()
{
return json_encode($this);
}
}
/*
* Instruct the phone to send one or more outgoing messages (SMS or USSD)
*/
class EnvayaSMS_Event_Send extends EnvayaSMS_Event
{
public $messages;
function __construct($messages /* array of EnvayaSMS_OutgoingMessage objects */)
{
$this->event = EnvayaSMS::EVENT_SEND;
$this->messages = $messages;
}
}
/*
* Update some of the app's settings.
*/
class EnvayaSMS_Event_Settings extends EnvayaSMS_Event
{
public $settings;
function __construct($settings /* associative array of key => value pairs (values can be int, bool, or string) */)
{
$this->event = EnvayaSMS::EVENT_SETTINGS;
$this->settings = $settings;
}
}
/*
* Cancel sending a message that was previously queued in the app via a 'send' event.
* Has no effect if the message has already been sent.
*/
class EnvayaSMS_Event_Cancel extends EnvayaSMS_Event
{
public $id;
function __construct($id /* id of previously created EnvayaSMS_OutgoingMessage object (string) */)
{
$this->event = EnvayaSMS::EVENT_CANCEL;
$this->id = $id;
}
}
/*
* Cancels all outgoing messages that are currently queued in the app. Incoming mesages are not affected.
*/
class EnvayaSMS_Event_CancelAll extends EnvayaSMS_Event
{
function __construct()
{
$this->event = EnvayaSMS::EVENT_CANCEL_ALL;
}
}
/*
* Appends a message to the app log.
*/
class EnvayaSMS_Event_Log extends EnvayaSMS_Event
{
public $message;
function __construct($message)
{
$this->event = EnvayaSMS::EVENT_LOG;
$this->message = $message;
}
}

View File

@ -10,13 +10,15 @@ PHP web server, by running the following commands:
git submodule init
php server.php
example/config.php contains the list of phone numbers and passwords for phones running EnvayaSMS.
example/config.php contains the password for a phone running EnvayaSMS. The password
This password must match the password in the EnvayaSMS app settings,
otherwise example/gateway.php will return an "Invalid password" error.
On a phone running EnvayaSMS, go to Menu -> Settings and enter:
* Server URL: The URL to example/www/index.php.
If you're using server.php, this will be http://<your_ip_address>:8002/
* Your phone number: One of the phone numbers listed in example/config.php
* Password: The corresponding password in example/config.php
* Server URL: The URL to example/www/gateway.php.
If you're using server.php, this will be http://<your_ip_address>:8002/gateway.php
* Your phone number: The phone number of your Android phone
* Password: The password in example/config.php
To send an outgoing SMS, use
php example/send_sms.php
@ -25,4 +27,4 @@ example/www/test.html allows you to simulate the HTTP requests made by EnvayaSMS
in your browser without actually using the EnvayaSMS app.
If you're using server.php, just go to http://<your_ip_address>:8002/test.html
See EnvayaSMS.php and example/www/index.php
See EnvayaSMS.php and example/www/gateway.php

View File

@ -1,9 +1,33 @@
<?php
$PASSWORDS = array(
'16505551212' => 'rosebud',
'16505551213' => 's3krit',
);
$PHONE_NUMBERS = array_keys($PASSWORDS);
ini_set('display_errors','0');
/*
* This password must match the password in the EnvayaSMS app settings,
* otherwise example/www/gateway.php will return an "Invalid request signature" error.
*/
$PASSWORD = 'rosebud';
/*
* example/send_sms.php uses the local file system to queue outgoing messages
* in this directory.
*/
$OUTGOING_DIR_NAME = __DIR__."/outgoing_sms";
/*
* AMQP allows you to send outgoing messages in real-time (i.e. 'push' instead of polling).
* In order to use AMQP, you would need to install an AMQP server such as RabbitMQ, and
* also enter the AMQP connection settings in the app. (The settings in the EnvayaSMS app
* should use the same vhost and queue name, but may use a different host/port/user/password.)
*/
$AMQP_SETTINGS = array(
'host' => 'localhost',
'port' => 5672,
'user' => 'guest',
'password' => 'guest',
'vhost' => '/',
'queue_name' => "envayasms"
);

@ -0,0 +1 @@
Subproject commit 056e94d28d14466c19a4eccb15f2d5e8e7ba017d

View File

@ -1,46 +1,33 @@
<?php
/*
* Command line script to simulate sending an outgoing SMS from the server.
* Command line script to send an outgoing SMS from the server.
*
* The message will be queued on the server until the next time
* EnvayaSMS checks for outgoing messages.
* This example script queues outgoing messages using the local filesystem.
* The messages are sent the next time EnvayaSMS sends an ACTION_OUTGOING request to www/gateway.php.
*/
require_once __DIR__."/config.php";
require_once dirname(__DIR__)."/EnvayaSMS.php";
$arg_len = sizeof($argv);
if ($arg_len == 4)
if (sizeof($argv) == 3)
{
$from = $argv[1];
$to = $argv[2];
$message = $argv[3];
}
else if ($arg_len == 3)
{
$from = $PHONE_NUMBERS[0];
$to = $argv[1];
$message = $argv[2];
$body = $argv[2];
}
else
{
error_log("Usage: php send_sms.php [<from>] <to> \"<message>\"");
error_log("Examples: ");
error_log(" php send_sms.php 16505551212 16504449876 \"hello world\"");
error_log("Usage: php send_sms.php <to> \"<message>\"");
error_log("Example: ");
error_log(" php send_sms.php 16504449876 \"hello world\"");
die;
}
$id = uniqid("");
$message = new EnvayaSMS_OutgoingMessage();
$message->id = uniqid("");
$message->to = $to;
$message->message = $body;
$filename = "$OUTGOING_DIR_NAME/$id.json";
file_put_contents("$OUTGOING_DIR_NAME/{$message->id}.json", json_encode($message));
file_put_contents($filename, json_encode(array(
'from' => $from,
'to' => $to,
'message' => $message,
'id' => $id
)));
echo "Message $id added to outgoing queue\n";
echo "Message {$message->id} added to filesystem queue\n";

View File

@ -0,0 +1,45 @@
<?php
/*
* Command line script to send an outgoing SMS from the server.
*
* Requires an AMQP server to be configured in config.php, and
* pushes SMS to the phone immediately using the real-time connection.
*/
require_once __DIR__."/config.php";
require_once dirname(__DIR__)."/EnvayaSMS.php";
require_once __DIR__."/php-amqplib/amqp.inc";
if (sizeof($argv) == 3)
{
$to = $argv[1];
$body = $argv[2];
}
else
{
error_log("Usage: php send_sms_amqp.php <to> \"<message>\"");
die;
}
$message = new EnvayaSMS_OutgoingMessage();
$message->id = uniqid("");
$message->to = $to;
$message->message = $body;
$conn = new AMQPConnection($AMQP_SETTINGS['host'], $AMQP_SETTINGS['port'],
$AMQP_SETTINGS['user'], $AMQP_SETTINGS['password'], $AMQP_SETTINGS['vhost']);
$ch = $conn->channel();
$ch->queue_declare($AMQP_SETTINGS['queue_name'], false, true, false, false);
$event = new EnvayaSMS_Event_Send(array($message));
$msg = new AMQPMessage($event->render(), array('content_type' => 'application/json', 'delivery-mode' => 2));
$ch->basic_publish($msg, '', $AMQP_SETTINGS['queue_name']);
$ch->close();
$conn->close();
echo "Message {$message->id} added to AMQP queue\n";

View File

@ -1,44 +1,38 @@
<?php
/*
* This example script implements the EnvayaSMS API.
*
* It sends an auto-reply to each incoming message, and sends outgoing SMS
* that were previously queued by example/send_sms.php .
*
* To use this file, set the URL to this file as as the the Server URL in the EnvayaSMS app.
* The password in the EnvayaSMS app settings must be the same as $PASSWORD in config.php.
*/
require_once dirname(__DIR__)."/config.php";
require_once dirname(dirname(__DIR__))."/EnvayaSMS.php";
ini_set('display_errors','0');
// this example implementation uses the filesystem to store outgoing SMS messages,
// but presumably a production implementation would use another storage method
$request = EnvayaSMS::get_request();
$phone_number = $request->phone_number;
header("Content-Type: {$request->get_response_type()}");
$password = @$PASSWORDS[$phone_number];
header("Content-Type: text/xml");
if (!isset($password) || !$request->is_validated($password))
if (!$request->is_validated($PASSWORD))
{
header("HTTP/1.1 403 Forbidden");
error_log("Invalid request signature");
echo EnvayaSMS::get_error_xml("Invalid request signature");
error_log("Invalid password");
echo $request->render_error_response("Invalid password");
return;
}
// append to EnvayaSMS app log
$app_log = $request->log;
if ($app_log)
{
$log_file = dirname(__DIR__)."/log/sms_".preg_replace('#[^\w]#', '', $request->phone_number).".log";
$f = fopen($log_file, "a");
fwrite($f, $app_log);
fclose($f);
}
$action = $request->get_action();
switch ($action->type)
{
case EnvayaSMS::ACTION_INCOMING:
// Send an auto-reply for each incoming message.
$type = strtoupper($action->message_type);
error_log("Received $type from {$action->from}");
@ -63,24 +57,28 @@ switch ($action->type)
error_log("Sending reply: {$reply->message}");
echo $action->get_response_xml(array($reply));
echo $request->render_response(array(
new EnvayaSMS_Event_Send(array($reply))
));
return;
case EnvayaSMS::ACTION_OUTGOING:
$messages = array();
// In this example implementation, outgoing SMS messages are queued
// on the local file system by send_sms.php.
$dir = opendir($OUTGOING_DIR_NAME);
while ($file = readdir($dir))
{
if (preg_match('#\.json$#', $file))
{
$data = json_decode(file_get_contents("$OUTGOING_DIR_NAME/$file"), true);
if ($data && @$data['from'] == $phone_number)
if ($data)
{
$sms = new EnvayaSMS_OutgoingMessage();
$sms->id = $data['id'];
$sms->to = $data['to'];
$sms->from = $data['from'];
$sms->message = $data['message'];
$messages[] = $sms;
}
@ -88,33 +86,40 @@ switch ($action->type)
}
closedir($dir);
echo $action->get_response_xml($messages);
$events = array();
if ($messages)
{
$events[] = new EnvayaSMS_Event_Send($messages);
}
echo $request->render_response($events);
return;
case EnvayaSMS::ACTION_SEND_STATUS:
$id = $action->id;
error_log("message $id status: {$action->status}");
// delete file with matching id
if (preg_match('#^\w+$#', $id) && unlink("$OUTGOING_DIR_NAME/$id.json"))
if (preg_match('#^\w+$#', $id))
{
echo EnvayaSMS::get_success_xml();
}
else
{
header("HTTP/1.1 404 Not Found");
echo EnvayaSMS::get_error_xml("Invalid id");
unlink("$OUTGOING_DIR_NAME/$id.json");
}
echo $request->render_response();
return;
case EnvayaSMS::ACTION_DEVICE_STATUS:
error_log("device_status = {$action->status}");
echo EnvayaSMS::get_success_xml();
echo $request->render_response();
return;
case EnvayaSMS::ACTION_TEST:
echo EnvayaSMS::get_success_xml();
echo $request->render_response();
return;
default:
header("HTTP/1.1 404 Not Found");
echo EnvayaSMS::get_error_xml("Invalid action");
echo $request->render_error_response("The server does not support the requested action.");
return;
}

View File

@ -0,0 +1,87 @@
<?php
/*
* An example implementation of the EnvayaSMS server API that uses AMQP
* to send outgoing messages in real-time, intended to be used together with
* example/send_sms_amqp.php.
*
* To use this file, set the URL to this file as as the the Server URL in the EnvayaSMS app.
* The password in the EnvayaSMS app settings must be the same as $PASSWORD in config.php.
*/
require_once dirname(__DIR__)."/config.php";
require_once dirname(dirname(__DIR__))."/EnvayaSMS.php";
$request = EnvayaSMS::get_request();
header("Content-Type: {$request->get_response_type()}");
if (!$request->is_validated($PASSWORD))
{
header("HTTP/1.1 403 Forbidden");
error_log("Invalid password");
echo $request->render_error_response("Invalid password");
return;
}
$action = $request->get_action();
switch ($action->type)
{
case EnvayaSMS::ACTION_INCOMING:
// Doesn't do anything with incoming messages
error_log("Received {$action->message_type} from {$action->from}: {$action->message}");
echo $request->render_response();
return;
case EnvayaSMS::ACTION_OUTGOING:
// Doesn't need to do anything when polling for outgoing messages
// since they should be sent via the AMQP connection.
// Optionally, you could use AMQP basic_get to retrieve any messages
// from the AMQP queue so that it works in both polling and push modes.
error_log("No messages here, use AMQP instead");
echo $request->render_response(array(
new EnvayaSMS_Event_Log("No messages via polling, use AMQP instead")
));
return;
case EnvayaSMS::ACTION_AMQP_STARTED:
// The main point of this action is to allow the server to kick off old
// AMQP connections (that weren't closed properly) before their heartbeat timeout
// expires. This makes it possible to use long heartbeat timeouts to maximize
// the phone's battery life.
// With RabbitMQ, this can be done using the management API:
// GET /queues/VHOST/QUEUE_NAME
// to get the connection name for each consumer other than the current one
// DELETE /connections/CONNECTION_NAME
// to close the connection for each consumer other than the current one
error_log("AMQP connection started with consumer tag {$action->consumer_tag}");
echo $request->render_response();
return;
case EnvayaSMS::ACTION_SEND_STATUS:
error_log("message {$action->id} status: {$action->status}");
echo $request->render_response();
return;
case EnvayaSMS::ACTION_DEVICE_STATUS:
error_log("device_status = {$action->status}");
echo $request->render_response();
return;
case EnvayaSMS::ACTION_TEST:
echo $request->render_response();
return;
default:
header("HTTP/1.1 404 Not Found");
echo $request->render_error_response("The server does not support the requested action.");
return;
}

View File

@ -31,13 +31,26 @@ body
<tr><th>Phone Number</th><td><input id='phone_number' type='text' /></td></tr>
<tr><th>Password</th><td><input id='password' type='password' /></td></tr>
<tr><th>Log Messages</th><td><textarea id='log' style='width:250px'></textarea></td></tr>
<tr><th>Send Limit</th><td><input id='send_limit' value='100' type='text' /></td></tr>
<tr><th>Settings Version</th><td><input id='settings_version' value='1' type='text' /></td></tr>
<tr><th>Battery</th><td><input id='battery' value='100' type='text' /></td></tr>
<tr><th>Power Source</th><td><select id='power'>
<option value='0'>0 (battery)</option>
<option value='1'>1 (USB)</option>
<option value='2'>2 (AC)</option>
</select></td></tr>
<tr><th>Network Type</th><td><input id='network' value='WIFI' type='text' /></td></tr>
<tr><th>Current Timestamp</th><td><input id='now' type='text' /></td></tr>
<tr><th>Action</th><td><select id='action' onchange='actionChanged()' onkeypress='actionChanged()'>
<option value='incoming'>incoming</option>
<option value='outgoing'>outgoing</option>
<option value='send_status'>send_status</option>
<option value='device_status'>device_status</option>
<option value='test'>test</option>
<option value='amqp_started'>amqp_started</option>
</select></td></tr>
</table>
<div id='action_incoming'>
@ -47,6 +60,7 @@ body
<tr><th>Message Type</th><td><select id='message_type'>
<option value='sms'>sms</option>
<option value='mms'>mms</option>
<option value='call'>call</option>
</select></td></tr>
<tr><th>Message</th><td><textarea id='message' style='width:250px'></textarea></td></tr>
<tr><th>Timestamp</th><td><input id='timestamp' type='text' /></td></tr>
@ -64,6 +78,7 @@ body
<option value='sent'>sent</option>
<option value='failed'>failed</option>
<option value='queued'>queued</option>
<option value='cancelled'>cancelled</option>
</select></td></tr>
<tr><th>Error Message</th><td><input id='error' type='text' size='50' /></td></tr>
</table>
@ -80,10 +95,16 @@ body
<option value='power_disconnected'>power_disconnected</option>
<option value='battery_low'>battery_low</option>
<option value='battery_okay'>battery_okay</option>
<option value='send_limit_exceeded'>send_limit_exceeded</option>
</select></td></tr>
</table>
</div>
<div id='action_amqp_started' style='display:none'>
<h4>Parameters for action=amqp_started:</h4>
<table class='smsTable'>
<tr><th>Consumer Tag</th><td><input id='consumer_tag' type='text' /></td></tr>
</table>
</div>
<script type='text/javascript'>
@ -110,10 +131,16 @@ function performAction() {
var action = $('action').value;
var params = {
version: '13',
version: '29',
phone_number: $('phone_number').value,
action: action,
log: $('log').value
log: $('log').value,
send_limit: $('send_limit').value,
settings_version: $('settings_version').value,
battery: $('battery').value,
power: $('power').value,
network: $('network').value,
now: $('now').value
};
if (action == 'incoming')
@ -133,6 +160,10 @@ function performAction() {
{
params.status = $('device_status').value;
}
else if (action == 'amqp_started')
{
params.status = $('consumer_tag').value;
}
var xhr = (window.ActiveXObject && !window.XMLHttpRequest) ? new ActiveXObject("Msxml2.XMLHTTP") : new XMLHttpRequest();
@ -189,8 +220,9 @@ function performAction() {
xhr.send(paramStr);
}
$('server_url').value = location.href.replace("test.html","");
$('server_url').value = location.href.replace("test.html","gateway.php");
$('timestamp').value = new Date().getTime();
$('now').value = new Date().getTime();
</script>

View File

@ -0,0 +1,464 @@
package org.envaya.sms;
import org.envaya.sms.service.AmqpConsumerService;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.net.wifi.WifiManager;
import android.os.SystemClock;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.QueueingConsumer;
import com.rabbitmq.client.AlreadyClosedException;
import com.rabbitmq.client.ShutdownSignalException;
import org.envaya.sms.receiver.StartAmqpConsumer;
import org.envaya.sms.service.AmqpHeartbeatService;
import org.envaya.sms.task.HttpTask;
import java.io.IOException;
import java.security.SecureRandom;
import java.util.concurrent.Delayed;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import org.apache.http.message.BasicNameValuePair;
import org.json.JSONException;
import org.json.JSONObject;
public class AmqpConsumer {
public static final long RETRY_DELAY = 5000; // ms
public static final long RETRY_ERROR_DELAY = 60000; // ms
public static final long MIN_EXPECTED_ERROR_DELAY = 10000; // ms
public static final int WIFI_MODE_FULL_HIGH_PERF = 3;
// constant not added until Android 3.1 but seems to work on older versions
// (at least 2.3)
protected Channel channel = null;
protected Connection connection;
protected App app;
protected ConsumeThread consumeThread;
protected WifiManager.WifiLock wifiLock;
protected WifiManager wifiManager;
protected AlarmManager alarmManager;
//protected PowerManager powerManager;
public AmqpConsumer(App app)
{
this.app = app;
wifiManager = (WifiManager) app.getSystemService(Context.WIFI_SERVICE);
alarmManager = (AlarmManager) app.getSystemService(Context.ALARM_SERVICE);
//powerManager = (PowerManager) app.getSystemService(Context.POWER_SERVICE);
}
// 'async' and 'delayed' methods must not be synchronized or app will deadlock ---
// StartAmqpConsumerService thread owns lock on AmqpConsumer and needs lock on App to call app.log,
// while main thread in onConnectivityChanged owns lock on App and must not take any locks on AmqpConsumer
public void startAsync()
{
startStopAsync(true);
}
public void stopAsync()
{
cancelStartDelayed();
startStopAsync(false);
}
public void startStopAsync(boolean start)
{
Intent intent = new Intent(app, AmqpConsumerService.class);
intent.putExtra("start", start);
app.startService(intent);
}
public void cancelStartDelayed()
{
alarmManager.cancel(getStartPendingIntent());
}
public PendingIntent getStartPendingIntent()
{
return PendingIntent.getBroadcast(app,
0,
new Intent(app, StartAmqpConsumer.class),
0);
}
public void startDelayed(long delay)
{
alarmManager.set(
AlarmManager.ELAPSED_REALTIME_WAKEUP,
SystemClock.elapsedRealtime() + delay,
getStartPendingIntent()
);
}
public synchronized void startBlocking()
{
stopBlocking();
boolean enabled = app.tryGetBooleanSetting("amqp_enabled", false);
if (!enabled)
{
app.log("Real-time connection disabled");
return;
}
String host = app.tryGetStringSetting("amqp_host", null);
int port = app.tryGetIntegerSetting("amqp_port", 0);
boolean ssl = app.tryGetBooleanSetting("amqp_ssl", false);
String vhost = app.tryGetStringSetting("amqp_vhost", null);
String username = app.tryGetStringSetting("amqp_user", null);
String password = app.tryGetStringSetting("amqp_password", null);
String queue = app.tryGetStringSetting("amqp_queue", null);
if (host == null || port == 0 || username == null || password == null || queue == null || vhost == null)
{
app.log("Real-time connection not configured");
return;
}
boolean started = tryStart(host, port, ssl, vhost, username, password, queue);
if (!started)
{
startDelayed(RETRY_ERROR_DELAY);
}
}
private Runnable heartbeatRunnable;
public synchronized void setHeartbeatRunnable(Runnable heartbeatRunnable)
{
this.heartbeatRunnable = heartbeatRunnable;
}
public synchronized void sendHeartbeatBlocking()
{
if (heartbeatRunnable != null)
{
heartbeatRunnable.run();
}
else
{
app.log("no heartbeat runnable");
}
}
public PendingIntent getHeartbeatPendingIntent()
{
return PendingIntent.getService(app,
0,
new Intent(app, AmqpHeartbeatService.class),
0);
}
public class HeartbeatExecutor extends ScheduledThreadPoolExecutor
{
public HeartbeatExecutor()
{
super(1);
}
@Override
public ScheduledFuture<?> scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit)
{
long delayMs = TimeUnit.MILLISECONDS.convert(initialDelay, unit);
long periodMs = TimeUnit.MILLISECONDS.convert(period, unit);
setHeartbeatRunnable(command);
alarmManager.setRepeating(
AlarmManager.ELAPSED_REALTIME_WAKEUP,
SystemClock.elapsedRealtime() + delayMs,
periodMs,
getHeartbeatPendingIntent());
//app.log("scheduleAtFixedRate " + delayMs + ", " + periodMs);
return new ScheduledFuture<Integer>()
{
public long getDelay(TimeUnit u2)
{
return 0;
}
public int compareTo(Delayed d2)
{
return 0;
}
public Integer get()
{
return null;
}
public Integer get(long timeout, TimeUnit unit)
{
return null;
}
public boolean cancel(boolean interrupt)
{
// doesn't do anything -- alarm cancelled by stopBlocking
cancelled = true;
return true;
}
private boolean cancelled;
public boolean isCancelled()
{
return cancelled;
}
public boolean isDone()
{
return cancelled;
}
};
}
}
private synchronized boolean tryStart(String host, int port, boolean ssl, String vhost, String username, String password, String queue)
{
app.log("Establishing real-time connection...");
if (wifiManager.isWifiEnabled())
{
if (wifiLock == null)
{
wifiLock = wifiManager.createWifiLock(WIFI_MODE_FULL_HIGH_PERF, "telerivet-amqp");
wifiLock.setReferenceCounted(false);
}
wifiLock.acquire();
}
try
{
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost(host);
connectionFactory.setPort(port);
connectionFactory.setVirtualHost(vhost);
connectionFactory.setUsername(username);
connectionFactory.setPassword(password);
/*
connectionFactory.setLogger(new IILogger() {
public void log(String str)
{
app.log(str);
}
});
*/
connectionFactory.setHeartbeatExecutor(new HeartbeatExecutor());
TrustManager[] trustManagers = null; // use built-in SSL certificate verification
if (ssl)
{
/*
// could customize SSL certificate verification
trustManagers = new TrustManager[]{
new X509TrustManager() {
public X509Certificate[] getAcceptedIssuers() {
return null;
}
public void checkClientTrusted(X509Certificate[] certs, String authType) {
}
public void checkServerTrusted(X509Certificate[] certs, String authType)
throws CertificateException
{
}
}
};
*/
SSLContext c = SSLContext.getInstance("TLS");
c.init(null, trustManagers, new SecureRandom());
connectionFactory.useSslProtocol(c);
}
// Need to periodically check if connection is still working
// to allow the client and server to detect broken connections
// after 2*heartbeat seconds
// AMQP heartbeat interval has a big effect on battery usage on idle phones.
// The CPU will wake up every heartbeat for 5 seconds.
// For the rest of the time, the phone stays in deep sleep (CPU off) and is automatically
// woken up whenever the server sends a packet.
connectionFactory.setRequestedHeartbeat(app.tryGetIntegerSetting("amqp_heartbeat", 300));
connection = connectionFactory.newConnection();
channel = connection.createChannel();
channel.queueDeclare(queue, true, false, false, null);
channel.basicQos(1);
QueueingConsumer consumer = new QueueingConsumer(channel);
channel.basicConsume(queue, false, consumer);
consumeThread = new ConsumeThread(consumer);
consumeThread.start();
HttpTask task = new HttpTask(app,
new BasicNameValuePair("action", App.ACTION_AMQP_STARTED),
new BasicNameValuePair("consumer_tag", consumer.getConsumerTag())
);
task.execute();
return true;
}
catch (Exception ex)
{
app.logError("Error establishing real-time connection", ex);
return false;
}
}
public synchronized void stopBlocking() {
try
{
if (wifiLock != null && wifiLock.isHeld())
{
wifiLock.release();
}
alarmManager.cancel(getHeartbeatPendingIntent());
heartbeatRunnable = null;
if (consumeThread != null)
{
consumeThread.terminate();
consumeThread = null;
}
if (channel != null)
{
channel = null;
}
if (connection != null)
{
try
{
connection.close(10000);
}
catch (AlreadyClosedException ex) {}
catch (ShutdownSignalException ex) {}
connection = null;
}
}
catch (IOException ex)
{
app.logError("Error stopping real-time connection", ex);
}
}
public class ConsumeThread extends Thread
{
private QueueingConsumer consumer;
private boolean terminated = false;
public ConsumeThread(QueueingConsumer consumer)
{
this.consumer = consumer;
}
public synchronized void terminate()
{
this.terminated = true;
}
public synchronized boolean isTerminated()
{
return terminated;
}
public void processMessage(QueueingConsumer.Delivery delivery)
{
String jsonStr = new String(delivery.getBody());
try
{
JSONObject json = new JSONObject(jsonStr);
JsonUtils.processEvent(json, app, null);
}
catch (JSONException ex)
{
app.logError(ex);
}
}
@Override
public void run() {
long startTime = SystemClock.elapsedRealtime();
try
{
app.log("Real-time connection established.");
Channel ch = consumer.getChannel();
while(true)
{
QueueingConsumer.Delivery delivery = consumer.nextDelivery();
if (isTerminated())
{
break;
}
ch.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
processMessage(delivery);
if (isTerminated())
{
break;
}
}
app.log("Real-time connection stopped.");
}
catch (Exception ex)
{
if (!isTerminated())
{
if (ex instanceof ShutdownSignalException)
{
//app.logError("Real-time connection interrupted", ex);
app.log("Real-time connection interrupted");
}
else
{
app.logError("Real-time connection interrupted", ex);
}
long age = SystemClock.elapsedRealtime() - startTime;
stopAsync();
if (age < MIN_EXPECTED_ERROR_DELAY)
{
startDelayed(RETRY_ERROR_DELAY);
}
else
{
startDelayed(RETRY_DELAY);
}
}
else
{
app.log("Real-time connection stopped.");
}
}
}
}
}

View File

@ -1,5 +1,6 @@
package org.envaya.sms;
import org.envaya.sms.service.EnabledChangedService;
import android.app.Activity;
import android.app.AlarmManager;
import android.app.Application;
@ -18,8 +19,6 @@ 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;
@ -54,9 +53,17 @@ public final class App extends Application {
public static final String ACTION_OUTGOING = "outgoing";
public static final String ACTION_INCOMING = "incoming";
public static final String ACTION_FORWARD_SENT = "forward_sent";
public static final String ACTION_SEND_STATUS = "send_status";
public static final String ACTION_DEVICE_STATUS = "device_status";
public static final String ACTION_TEST = "test";
public static final String ACTION_AMQP_STARTED = "amqp_started";
public static final String EVENT_SEND = "send";
public static final String EVENT_CANCEL = "cancel";
public static final String EVENT_CANCEL_ALL = "cancel_all";
public static final String EVENT_LOG = "log";
public static final String EVENT_SETTINGS = "settings";
public static final String STATUS_QUEUED = "queued";
public static final String STATUS_FAILED = "failed";
@ -77,6 +84,9 @@ public final class App extends Application {
// intent to signal to Main activity (if open) that log has changed
public static final String LOG_CHANGED_INTENT = "org.envaya.sms.LOG_CHANGED";
public static final String SETTINGS_CHANGED_INTENT = "org.envaya.sms.SETTINGS_CHANGED";
public static final String EXPANSION_PACKS_CHANGED_INTENT = "org.envaya.sms.EXPANSION_PACKS_CHANGED";
// signal to PendingMessages activity (if open) that inbox/outbox has changed
public static final String INBOX_CHANGED_INTENT = "org.envaya.sms.INBOX_CHANGED";
@ -134,7 +144,8 @@ public final class App extends Application {
public final Queue<HttpTask> queuedTasks = new LinkedList<HttpTask>();
private SharedPreferences settings;
private MmsObserver mmsObserver;
private MessagingObserver messagingObserver;
private SpannableStringBuilder displayedLog = new SpannableStringBuilder();
private long lastLogTime;
@ -151,8 +162,10 @@ public final class App extends Application {
// count to provide round-robin selection of expansion packs
private int outgoingMessageCount = -1;
private MmsUtils mmsUtils;
private MessagingUtils messagingUtils;
private CallListener callListener;
private DatabaseHelper dbHelper;
private AmqpConsumer amqpConsumer;
private boolean connectivityError = false;
@ -161,14 +174,27 @@ public final class App extends Application {
{
super.onCreate();
// workaround for http://code.google.com/p/android/issues/detail?id=20915
try
{
Class.forName("android.os.AsyncTask");
}
catch (ClassNotFoundException ex)
{
}
settings = PreferenceManager.getDefaultSharedPreferences(this);
mmsUtils = new MmsUtils(this);
messagingUtils = new MessagingUtils(this);
callListener = new CallListener(this);
outgoingMessagePackages.add(getPackageName());
mmsObserver = new MmsObserver(this);
messagingObserver = new MessagingObserver(this);
dbHelper = new DatabaseHelper(this);
amqpConsumer = new AmqpConsumer(this);
try
{
@ -187,85 +213,18 @@ public final class App extends Application {
public void configuredChanged()
{
log(Html.fromHtml(
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)
if (isConfigured())
{
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);
}
sendBroadcast(new Intent(App.SETTINGS_CHANGED_INTENT));
enabledChanged();
}
if (isTestMode())
{
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();
}
public void enabledChanged()
{
TelephonyManager telephony = (TelephonyManager)
getSystemService(Context.TELEPHONY_SERVICE);
if (isEnabled())
{
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));
// startup/shutdown tasks may be slow, so offload them to a worker thread...
// IntentService takes care of only running one request at a time
startService(new Intent(this, EnabledChangedService.class));
}
public PackageInfo getPackageInfo()
@ -273,6 +232,11 @@ public final class App extends Application {
return packageInfo;
}
public boolean isSmsExpansionPackInstalled(String packageName)
{
return outgoingMessagePackages.contains(packageName);
}
public synchronized String chooseOutgoingSmsPackage(int numParts)
{
outgoingMessageCount++;
@ -388,8 +352,9 @@ public final class App extends Application {
if (prevLimit != newLimit)
{
log("Outgoing SMS limit: " + newLimit + " messages/hour");
log("Outgoing SMS rate limit: " + newLimit + " messages/hour");
}
sendBroadcast(new Intent(App.EXPANSION_PACKS_CHANGED_INTENT));
}
public int getOutgoingMessageLimit()
@ -442,11 +407,11 @@ public final class App extends Application {
{
String serverUrl = getServerUrl();
if (serverUrl.length() > 0) {
log("Checking for outgoing messages");
log("Checking for messages");
pollActive = true;
new PollerTask(this).execute();
} else {
log("Can't check outgoing messages; server URL not set");
log("Can't check messages; server URL not set");
}
}
else
@ -491,11 +456,20 @@ public final class App extends Application {
}
}
public boolean isConfigured()
{
return getServerUrl().length() > 0;
}
public boolean callNotificationsEnabled()
{
return tryGetBooleanSetting("call_notifications", false);
}
public String getConfigureServer() {
return settings.getString("configure_server", "");
}
public String getServerUrl() {
return settings.getString("server_url", "");
}
@ -504,8 +478,23 @@ public final class App extends Application {
return settings.getString("phone_number", "");
}
public int getOutgoingPollSeconds() {
return Integer.parseInt(settings.getString("outgoing_interval", "0"));
public boolean isAmqpEnabled()
{
return tryGetBooleanSetting("amqp_enabled", false);
}
public String getPhoneID() {
return settings.getString("phone_id", "");
}
public String getPhoneToken()
{
return settings.getString("phone_token", "");
}
public int getOutgoingPollSeconds()
{
return tryGetIntegerSetting("outgoing_interval", 0);
}
public boolean isEnabled()
@ -513,13 +502,30 @@ public final class App extends Application {
return tryGetBooleanSetting("enabled", false);
}
public String tryGetStringSetting(String name, String defaultValue)
{
return settings.getString(name, defaultValue);
}
public int tryGetIntegerSetting(String name, int defaultValue)
{
try
{
return settings.getInt(name, defaultValue);
}
catch (ClassCastException ex)
{
return Integer.parseInt(settings.getString(name, "" + defaultValue));
}
}
public boolean tryGetBooleanSetting(String name, boolean defaultValue)
{
try
{
return settings.getBoolean(name, defaultValue);
}
catch (Exception ex)
catch (ClassCastException ex)
{
return defaultValue;
}
@ -530,11 +536,21 @@ public final class App extends Application {
return tryGetBooleanSetting("network_failover", false);
}
public boolean isForwardingSentMessagesEnabled()
{
return tryGetBooleanSetting("forward_sent", false);
}
public boolean isTestMode()
{
return tryGetBooleanSetting("test_mode", false);
}
public boolean autoAddTestNumber()
{
return tryGetBooleanSetting("auto_add_test_number", false);
}
public boolean getKeepInInbox()
{
return tryGetBooleanSetting("keep_in_inbox", false);
@ -542,12 +558,17 @@ public final class App extends Application {
public boolean ignoreShortcodes()
{
return tryGetBooleanSetting("ignore_shortcodes", true);
return tryGetBooleanSetting("ignore_shortcodes", false);
}
public boolean ignoreNonNumeric()
{
return tryGetBooleanSetting("ignore_non_numeric", true);
return tryGetBooleanSetting("ignore_non_numeric", false);
}
public int getSettingsVersion()
{
return tryGetIntegerSetting("settings_version", 0);
}
public String getPassword() {
@ -628,12 +649,22 @@ public final class App extends Application {
sendBroadcast(new Intent(App.LOG_CHANGED_INTENT));
}
public boolean isUpgradeAvailable()
{
return tryGetIntegerSetting("market_version", 0) > packageInfo.versionCode;
}
public String getMarketVersionName()
{
return settings.getString("market_version_name", "?");
}
/*
* Changes whenever we change the beginning of the displayed log.
* If it doesn't change, the Main activity can update the log view much
* faster by using TextView.append() instead of TextView.setText()
*/
public int getLogEpoch()
public synchronized int getLogEpoch()
{
return logEpoch;
}
@ -665,9 +696,9 @@ public final class App extends Application {
}
}
public MmsUtils getMmsUtils()
public MessagingUtils getMessagingUtils()
{
return mmsUtils;
return messagingUtils;
}
private List<String> testPhoneNumbers;
@ -763,20 +794,48 @@ public final class App extends Application {
return values;
}
public boolean isPhoneNumberInList(String phoneNumber, List<String> phoneNumbers)
{
int phoneLen = phoneNumber.length();
for (String otherNumber : phoneNumbers)
{
if (otherNumber == null)
{
continue;
}
if (phoneNumber.equals(otherNumber))
{
return true;
}
int otherLen = otherNumber.length();
// fuzzy matching to account for different versions of same phone number (+, area codes, country codes)
if ((otherLen >= 7 && phoneNumber.endsWith(otherNumber)) ||
phoneLen >= 7 && otherNumber.endsWith(phoneNumber))
{
return true;
}
}
return false;
}
public boolean isForwardablePhoneNumber(String phoneNumber)
{
if (isTestMode())
{
return getTestPhoneNumbers().contains(phoneNumber);
return isPhoneNumberInList(phoneNumber, getTestPhoneNumbers());
}
if (getIgnoredPhoneNumbers().contains(phoneNumber))
if (isPhoneNumberInList(phoneNumber, getIgnoredPhoneNumbers()))
{
return false;
}
int numDigits = 0;
int length = phoneNumber.length();
int length = (phoneNumber == null) ? 0 : phoneNumber.length();
for (int i = 0; i < length; i++)
{
@ -949,9 +1008,13 @@ public final class App extends Application {
if (networkInfo == null || !networkInfo.isConnected())
{
amqpConsumer.stopAsync();
return;
}
amqpConsumer.startDelayed(5000);
int networkType = networkInfo.getType();
if (networkType == activeNetworkType)
@ -1008,4 +1071,24 @@ public final class App extends Application {
{
queuedTasks.add(task);
}
public DatabaseHelper getDatabaseHelper()
{
return dbHelper;
}
public MessagingObserver getMessagingObserver()
{
return messagingObserver;
}
public CallListener getCallListener()
{
return callListener;
}
public AmqpConsumer getAmqpConsumer()
{
return amqpConsumer;
}
}

View File

@ -1,57 +0,0 @@
package org.envaya.sms;
import android.app.IntentService;
import android.content.Intent;
import java.util.List;
public class CheckMmsInboxService extends IntentService
{
private App app;
private MmsUtils mmsUtils;
public CheckMmsInboxService(String name)
{
super(name);
}
public CheckMmsInboxService()
{
this("CheckMmsInboxService");
}
@Override
public void onCreate() {
super.onCreate();
app = (App)this.getApplicationContext();
mmsUtils = app.getMmsUtils();
}
@Override
protected void onHandleIntent(Intent intent)
{
List<IncomingMms> messages = mmsUtils.getMessagesInInbox();
for (IncomingMms mms : messages)
{
if (mmsUtils.isNewMms(mms))
{
app.log("New MMS id=" + mms.getId() + " in inbox");
// prevent forwarding MMS messages that existed in inbox
// before EnvayaSMS started, or re-forwarding MMS multiple
// times if we don't delete them.
mmsUtils.markOldMms(mms);
if (mms.isForwardable())
{
app.inbox.forwardMessage(mms);
}
else
{
app.log("Ignoring incoming MMS from " + mms.getFrom());
}
}
}
}
}

View File

@ -0,0 +1,270 @@
package org.envaya.sms;
import android.content.ContentValues;
import android.database.Cursor;
import android.database.SQLException;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
public class DatabaseHelper extends SQLiteOpenHelper {
public static final String DATABASE_NAME = "envayasms.db";
public static final int DATABASE_VERSION = 4;
private App app;
public DatabaseHelper(App app)
{
super(app, DATABASE_NAME, null, DATABASE_VERSION);
this.app = app;
}
public void onCreate(SQLiteDatabase db)
{
// Persisted backup of incoming messages have not been forwarded to server.
// allows us to restore pending messages after restart if phone runs out of batteries
// or app crashes.
db.execSQL("CREATE TABLE pending_incoming_messages ("
+ "`_id` INTEGER PRIMARY KEY AUTOINCREMENT,"
+ "`message_type` VARCHAR,"
+ "`messaging_id` INTEGER," // id in Messaging app database (if applicable)
+ "`from_number` VARCHAR,"
+ "`to_number` VARCHAR,"
+ "`message` TEXT,"
+ "`direction` INTEGER,"
+ "`timestamp` INTEGER"
+ ")");
db.execSQL("CREATE TABLE pending_outgoing_messages ("
+ "`_id` INTEGER PRIMARY KEY AUTOINCREMENT,"
+ "`message_type` VARCHAR,"
+ "`from_number` VARCHAR,"
+ "`to_number` VARCHAR,"
+ "`message` TEXT,"
+ "`priority` INTEGER,"
+ "`server_id` TEXT"
+ ")");
}
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion)
{
db.execSQL("DROP TABLE IF EXISTS pending_incoming_messages");
db.execSQL("DROP TABLE IF EXISTS pending_outgoing_messages");
onCreate(db);
}
public synchronized void restorePendingMessages()
{
restorePendingIncomingMessages();
restorePendingOutgoingMessages();
}
public synchronized void restorePendingIncomingMessages()
{
SQLiteDatabase db = getReadableDatabase();
Cursor c = db.query("pending_incoming_messages", null, null, null, null, null, null);
int idIndex = c.getColumnIndex("_id");
int messageTypeIndex = c.getColumnIndex("message_type");
int messagingIdIndex = c.getColumnIndex("messaging_id");
int fromIndex = c.getColumnIndex("from_number");
int toIndex = c.getColumnIndex("to_number");
int messageIndex = c.getColumnIndex("message");
int directionIndex = c.getColumnIndex("direction");
int timestampIndex = c.getColumnIndex("timestamp");
while (c.moveToNext())
{
long id = c.getLong(idIndex);
String messageType = c.getString(messageTypeIndex);
long messagingId = c.getLong(messagingIdIndex);
IncomingMessage message;
if (App.MESSAGE_TYPE_SMS.equals(messageType))
{
message = new IncomingSms(app);
}
else if (App.MESSAGE_TYPE_MMS.equals(messageType))
{
message = new IncomingMms(app);
}
else if (App.MESSAGE_TYPE_CALL.equals(messageType))
{
message = new IncomingCall(app);
}
else
{
app.log("Unknown message type " + messageType);
continue;
}
message.setMessagingId(messagingId);
message.setPersistedId(id);
message.setMessageBody(c.getString(messageIndex));
message.setFrom(c.getString(fromIndex));
message.setTo(c.getString(toIndex));
int directionInt = c.getInt(directionIndex);
IncomingMessage.Direction direction = (directionInt == IncomingMessage.Direction.Sent.ordinal()) ?
IncomingMessage.Direction.Sent : IncomingMessage.Direction.Incoming;
message.setDirection(direction);
message.setTimestamp(c.getLong(timestampIndex));
app.inbox.forwardMessage(message);
}
c.close();
}
public synchronized void restorePendingOutgoingMessages()
{
SQLiteDatabase db = getReadableDatabase();
Cursor c = db.query("pending_outgoing_messages", null, null, null, null, null, null);
int idIndex = c.getColumnIndex("_id");
int messageTypeIndex = c.getColumnIndex("message_type");
int fromIndex = c.getColumnIndex("from_number");
int toIndex = c.getColumnIndex("to_number");
int messageIndex = c.getColumnIndex("message");
int priorityIndex = c.getColumnIndex("priority");
int serverIdIndex = c.getColumnIndex("server_id");
while (c.moveToNext())
{
long id = c.getLong(idIndex);
String messageType = c.getString(messageTypeIndex);
OutgoingMessage message;
if (App.MESSAGE_TYPE_SMS.equals(messageType))
{
message = new OutgoingSms(app);
}
else
{
app.log("Unknown message type " + messageType);
continue;
}
message.setPersistedId(id);
message.setMessageBody(c.getString(messageIndex));
message.setFrom(c.getString(fromIndex));
String to = c.getString(toIndex);
if (to == null || "null".equals(to))
{
continue;
}
message.setTo(to);
message.setPriority(c.getInt(priorityIndex));
message.setServerId(c.getString(serverIdIndex));
app.outbox.sendMessage(message);
}
c.close();
}
public synchronized void insertPendingMessage(IncomingMessage message)
{
if (message.isPersisted())
{
return;
}
SQLiteDatabase db = getWritableDatabase();
ContentValues values = new ContentValues();
values.put("message_type", message.getMessageType());
values.put("messaging_id", message.getMessagingId());
values.put("from_number", message.getFrom());
values.put("to_number", message.getTo());
values.put("message", message.getMessageBody());
values.put("direction", message.getDirection().ordinal());
values.put("timestamp", message.getTimestamp());
try
{
long messageId = db.insertOrThrow("pending_incoming_messages", null, values);
message.setPersistedId(messageId);
}
catch (SQLException ex)
{
app.logError("Error saving message to database", ex);
}
}
public synchronized void insertPendingMessage(OutgoingMessage message)
{
if (message.isPersisted())
{
return;
}
SQLiteDatabase db = getWritableDatabase();
ContentValues values = new ContentValues();
values.put("message_type", message.getMessageType());
values.put("from_number", message.getFrom());
values.put("to_number", message.getTo());
values.put("priority", message.getPriority());
values.put("message", message.getMessageBody());
values.put("server_id", message.getServerId());
try
{
long messageId = db.insertOrThrow("pending_outgoing_messages", null, values);
message.setPersistedId(messageId);
}
catch (SQLException ex)
{
app.logError("Error saving message to database", ex);
}
}
public synchronized void deletePendingMessage(IncomingMessage message)
{
if (!message.isPersisted())
{
return;
}
SQLiteDatabase db = getWritableDatabase();
int rows = db.delete("pending_incoming_messages", "_id = ?", new String[] {"" + message.getPersistedId() });
if (rows == 0)
{
app.log("Error deleting pending message from database");
}
else
{
message.setPersistedId(0);
}
}
public synchronized void deletePendingMessage(OutgoingMessage message)
{
if (!message.isPersisted())
{
return;
}
SQLiteDatabase db = getWritableDatabase();
int rows = db.delete("pending_outgoing_messages", "_id = ?", new String[] {"" + message.getPersistedId() });
if (rows == 0)
{
app.log("Error deleting pending message from database");
}
else
{
message.setPersistedId(0);
}
}
}

View File

@ -34,7 +34,9 @@ public class Inbox {
}
incomingMessages.put(uri, message);
app.log("Received "+message.getDescription());
app.log("Received "+message.getDisplayType());
app.getDatabaseHelper().insertPendingMessage(message);
enqueueMessage(message);
}
@ -84,6 +86,8 @@ public class Inbox {
numForwardingMessages--;
}
app.getDatabaseHelper().deletePendingMessage(message);
app.log(message.getDescription() + " deleted");
notifyChanged();
}
@ -99,6 +103,7 @@ public class Inbox {
notifyChanged();
numForwardingMessages--;
maybeDequeueMessage();
}
@ -111,17 +116,12 @@ public class Inbox {
notifyChanged();
if (message instanceof IncomingMms)
{
IncomingMms mms = (IncomingMms)message;
if (!app.getKeepInInbox())
{
app.log("Deleting MMS " + mms.getId() + " from inbox...");
app.getMmsUtils().deleteFromInbox(mms);
}
}
message.onForwardComplete();
numForwardingMessages--;
app.getDatabaseHelper().deletePendingMessage(message);
maybeDequeueMessage();
}

View File

@ -5,25 +5,17 @@
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()
public IncomingCall(App app)
{
long id = nextId;
nextId++;
return id;
super(app);
}
public String getDisplayType()
@ -44,6 +36,6 @@ public class IncomingCall extends IncomingMessage {
public Uri getUri()
{
return Uri.withAppendedPath(App.INCOMING_URI, "call/" + id);
return Uri.withAppendedPath(App.INCOMING_URI, "call/" + Uri.encode(from) + "/" + timestamp);
}
}

View File

@ -8,14 +8,29 @@ import org.apache.http.message.BasicNameValuePair;
public abstract class IncomingMessage extends QueuedMessage {
protected String from;
protected String from; // phone number of other party, for messages with direction=Direction.Incoming
protected String to; // phone number of other party, for messages with direction=Direction.Sent
protected Direction direction = Direction.Incoming;
protected long messagingId; // _id from Messaging app content provider tables (if applicable)
protected String message = "";
protected long timestamp; // unix timestamp in milliseconds
protected long timeReceived; // SystemClock.elapsedRealtime
protected long timeCreated; // SystemClock.elapsedRealtime
private ProcessingState state = ProcessingState.None;
public enum Direction
{
Incoming, // a message that was received by this phone
Sent // Message was sent via Messaging app (so it's "Incoming" from
// the phone to the server, but we don't actually send it)
}
public enum ProcessingState
{
None, // not doing anything with this sms now... just sitting around
@ -25,13 +40,33 @@ public abstract class IncomingMessage extends QueuedMessage {
Forwarded
}
public IncomingMessage(App app)
{
super(app);
}
public IncomingMessage(App app, String from, long timestamp)
{
super(app);
if (from == null)
{
from = "";
}
this.from = from;
this.timestamp = timestamp;
this.timeCreated = SystemClock.elapsedRealtime();
}
this.timeReceived = SystemClock.elapsedRealtime();
public void setDirection(Direction direction)
{
this.direction = direction;
}
public Direction getDirection()
{
return this.direction;
}
public String getMessageBody()
@ -39,9 +74,14 @@ public abstract class IncomingMessage extends QueuedMessage {
return message;
}
public void setMessageBody(String message)
{
this.message = message;
}
public long getAge()
{
return SystemClock.elapsedRealtime() - timeReceived;
return SystemClock.elapsedRealtime() - timeCreated;
}
public long getTimestamp()
@ -49,6 +89,11 @@ public abstract class IncomingMessage extends QueuedMessage {
return timestamp;
}
public void setTimestamp(long timestamp)
{
this.timestamp = timestamp;
}
public ProcessingState getProcessingState()
{
return state;
@ -61,7 +106,14 @@ public abstract class IncomingMessage extends QueuedMessage {
public boolean isForwardable()
{
return app.isForwardablePhoneNumber(from);
if (direction == Direction.Sent)
{
return app.isForwardingSentMessagesEnabled() && app.isForwardablePhoneNumber(to);
}
else
{
return app.isForwardablePhoneNumber(from);
}
}
public String getFrom()
@ -69,6 +121,32 @@ public abstract class IncomingMessage extends QueuedMessage {
return from;
}
public void setFrom(String from)
{
this.from = from;
}
public String getTo()
{
return to;
}
public void setTo(String to)
{
this.to = to;
}
public long getMessagingId()
{
return messagingId;
}
public void setMessagingId(long messagingId)
{
this.messagingId = messagingId;
}
protected Intent getRetryIntent() {
Intent intent = new Intent(app, IncomingMessageRetry.class);
intent.setData(this.getUri());
@ -92,7 +170,14 @@ public abstract class IncomingMessage extends QueuedMessage {
public String getDescription()
{
return getDisplayType() + " from " + getFrom();
if (direction == Direction.Sent)
{
return "Sent " + getDisplayType() + " to " + getTo();
}
else
{
return getDisplayType() + " from " + getFrom();
}
}
public void tryForwardToServer()
@ -107,15 +192,30 @@ public abstract class IncomingMessage extends QueuedMessage {
public abstract String getMessageType();
public void onForwardComplete()
{
}
protected ForwarderTask getForwarderTask()
{
return new ForwarderTask(this,
ForwarderTask task = 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())
new BasicNameValuePair("timestamp", "" + getTimestamp())
);
if (direction == Direction.Sent)
{
task.addParam("action", App.ACTION_FORWARD_SENT);
task.addParam("to", getTo());
}
else
{
task.addParam("action", App.ACTION_INCOMING);
task.addParam("from", getFrom());
}
return task;
}
}

View File

@ -14,15 +14,42 @@ import org.apache.http.entity.mime.content.ContentBody;
import org.envaya.sms.task.ForwarderTask;
public class IncomingMms extends IncomingMessage {
List<MmsPart> parts;
long id;
String contentLocation;
private List<MmsPart> parts;
public IncomingMms(App app, String from, long timestamp, long id)
public IncomingMms(App app, long timestamp, long messagingId)
{
super(app, from, timestamp);
this.parts = new ArrayList<MmsPart>();
this.id = id;
super(app, null, timestamp);
this.messagingId = messagingId;
}
public IncomingMms(App app)
{
super(app);
}
@Override
public String getFrom()
{
if (from == null || from.length() == 0)
{
// lazy-load sender number from Messaging database as needed
from = app.getMessagingUtils().getMmsSenderNumber(messagingId);
}
return from;
}
public List<MmsPart> getParts()
{
if (parts == null)
{
// lazy-load mms parts from Messaging database as needed
this.parts = new ArrayList<MmsPart>();
for (MmsPart part : app.getMessagingUtils().getMmsParts(messagingId))
{
parts.add(part);
}
}
return parts;
}
public String getDisplayType()
@ -30,43 +57,17 @@ public class IncomingMms extends IncomingMessage {
return "MMS";
}
public List<MmsPart> getParts()
{
return parts;
}
public void addPart(MmsPart part)
{
parts.add(part);
}
public long getId()
{
return id;
}
public String getContentLocation()
{
return contentLocation;
}
public void setContentLocation(String contentLocation)
{
this.contentLocation = contentLocation;
}
@Override
public String toString()
{
StringBuilder builder = new StringBuilder();
builder.append("MMS id=");
builder.append(id);
builder.append(messagingId);
builder.append(" from=");
builder.append(from);
builder.append(":\n");
for (MmsPart part : parts)
for (MmsPart part : getParts())
{
builder.append(" ");
builder.append(part.toString());
@ -84,7 +85,7 @@ public class IncomingMms extends IncomingMessage {
JSONArray partsMetadata = new JSONArray();
for (MmsPart part : parts)
for (MmsPart part : getParts())
{
String formFieldName = "part" + i;
String text = part.getText();
@ -148,7 +149,7 @@ public class IncomingMms extends IncomingMessage {
@Override
public String getMessageBody()
{
for (MmsPart part : parts)
for (MmsPart part : getParts())
{
if ("text/plain".equals(part.getContentType()))
{
@ -161,11 +162,21 @@ public class IncomingMms extends IncomingMessage {
public Uri getUri()
{
return Uri.withAppendedPath(App.INCOMING_URI, "mms/" + id);
return Uri.withAppendedPath(App.INCOMING_URI, "mms/" + messagingId);
}
public String getMessageType()
{
return App.MESSAGE_TYPE_MMS;
}
@Override
public void onForwardComplete()
{
if (!app.getKeepInInbox())
{
app.log("Deleting MMS " + getMessagingId() + " from inbox...");
app.getMessagingUtils().deleteFromMmsInbox(this);
}
}
}

View File

@ -5,7 +5,6 @@ import android.net.Uri;
import android.telephony.SmsMessage;
import java.security.InvalidParameterException;
import java.util.List;
import org.envaya.sms.task.ForwarderTask;
public class IncomingSms extends IncomingMessage {
@ -35,10 +34,9 @@ public class IncomingSms extends IncomingMessage {
}
}
// constructor for SMS retrieved from Messaging inbox
public IncomingSms(App app, String from, String message, long timestampMillis) {
super(app, from, timestampMillis);
this.message = message;
public IncomingSms(App app)
{
super(app);
}
public String getDisplayType()

View File

@ -0,0 +1,196 @@
package org.envaya.sms;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import org.apache.commons.io.IOUtils;
import org.apache.http.HttpResponse;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
public class JsonUtils {
public static JSONObject parseResponse(HttpResponse response)
throws IOException, JSONException
{
String responseBody = IOUtils.toString(response.getEntity().getContent(), "UTF-8");
return new JSONObject(responseBody);
}
public static String getErrorText(JSONObject json)
{
JSONObject errorObject = json.optJSONObject("error");
return errorObject != null ? errorObject.optString("message") : null;
}
public static void processEvents(JSONObject json, App app, String defaultTo)
throws JSONException
{
JSONArray events = json.optJSONArray("events");
if (events != null)
{
int numEvents = events.length();
for (int i = 0; i < numEvents; i++)
{
JsonUtils.processEvent(events.getJSONObject(i), app, defaultTo);
}
}
}
public static void processEvent(JSONObject json, App app, String defaultTo)
throws JSONException
{
String event = json.getString("event");
if (App.EVENT_SEND.equals(event))
{
for (OutgoingMessage message : JsonUtils.getMessagesList(json, app, defaultTo))
{
app.outbox.sendMessage(message);
}
}
else if (App.EVENT_LOG.equals(event))
{
app.log(json.getString("message"));
}
else if (App.EVENT_SETTINGS.equals(event))
{
JsonUtils.updateSettings(json, app);
}
else if (App.EVENT_CANCEL.equals(event))
{
String id = json.getString("id");
OutgoingMessage message = app.outbox.getMessage(OutgoingMessage.getUriForServerId(id));
if (message != null && message.isCancelable())
{
app.outbox.deleteMessage(message);
app.outbox.maybeDequeueMessage();
}
}
else if (App.EVENT_CANCEL_ALL.equals(event))
{
for (OutgoingMessage message : app.outbox.getMessages())
{
if (message.isCancelable())
{
app.outbox.deleteMessage(message);
}
}
}
else
{
app.log("Unknown event" + event);
}
}
public static void updateSettings(JSONObject json, App app)
throws JSONException
{
JSONObject settingsObject = json.optJSONObject("settings");
if (settingsObject != null && settingsObject.length() > 0)
{
SharedPreferences.Editor settings = PreferenceManager.getDefaultSharedPreferences(app).edit();
Iterator it = settingsObject.keys();
while (it.hasNext())
{
String name = (String)it.next();
Object value = settingsObject.get(name);
if (value instanceof String)
{
settings.putString(name, (String)value);
}
else if (value instanceof Boolean)
{
settings.putBoolean(name, (Boolean)value);
}
else if (value instanceof Integer)
{
settings.putInt(name, (Integer)value);
}
else
{
app.log("Unknown setting type " + value.getClass().getName() + " for name " + name);
}
}
settings.commit();
app.log("Updated app settings");
app.configuredChanged();
}
}
static App app;
// like JSONObject.optString but doesn't convert null to "null"
private static String optString(JSONObject json, String key, String defaultValue)
{
try
{
Object value = json.get(key);
if (value == null || JSONObject.NULL.equals(value))
{
return defaultValue;
}
else
{
return value.toString();
}
}
catch (JSONException ex)
{
return defaultValue;
}
}
public static List<OutgoingMessage> getMessagesList(JSONObject json, App app, String defaultTo)
throws JSONException
{
List<OutgoingMessage> messages = new ArrayList<OutgoingMessage>();
JSONArray messagesList = json.optJSONArray("messages");
JsonUtils.app = app;
if (messagesList != null)
{
int numMessages = messagesList.length();
for (int i = 0; i < numMessages; i++)
{
JSONObject messageObject = messagesList.getJSONObject(i);
OutgoingMessage message = OutgoingMessage.newFromMessageType(app,
optString(messageObject, "type", App.MESSAGE_TYPE_SMS));
message.setFrom(app.getPhoneNumber());
String to = optString(messageObject, "to", defaultTo);
if (to == null || "".equals(to) || "null".equals(to))
{
app.log("Received invalid SMS from server (missing recipient)");
continue;
}
String body = optString(messageObject, "message","");
message.setTo(to);
message.setServerId(optString(messageObject, "id", null));
message.setPriority(messageObject.optInt("priority", 0));
message.setMessageBody(body);
messages.add(message);
}
}
return messages;
}
}

View File

@ -0,0 +1,54 @@
package org.envaya.sms;
import org.envaya.sms.service.CheckMessagingService;
import android.content.Intent;
import android.database.ContentObserver;
import android.net.Uri;
import android.os.Handler;
import java.util.List;
public final class MessagingObserver extends ContentObserver {
// constants from android.provider.Telephony
public static final Uri OBSERVER_URI = Uri.parse("content://mms-sms/");
private App app;
public MessagingObserver(App app) {
super(new Handler());
this.app = app;
}
public void register()
{
app.getContentResolver().registerContentObserver(OBSERVER_URI, true, this);
MessagingUtils messagingUtils = app.getMessagingUtils();
for (IncomingMms mms : messagingUtils.getMessagesInMmsInbox())
{
messagingUtils.markSeenMms(mms);
}
for (IncomingSms sms : messagingUtils.getSentSmsMessages())
{
messagingUtils.markSeenSentSms(sms);
}
}
public void unregister()
{
app.getContentResolver().unregisterContentObserver(this);
}
@Override
public void onChange(final boolean selfChange) {
super.onChange(selfChange);
if (!selfChange)
{
// check MMS inbox in an IntentService since it may be slow
// and we only want to do one check at a time
app.startService(new Intent(app, CheckMessagingService.class));
}
}
}

View File

@ -7,39 +7,42 @@ import android.net.Uri;
import java.util.*;
/*
* Utilities for parsing IncomingMms from the MMS content provider tables,
* as defined by android.provider.Telephony
* Utilities for parsing MMS and SMS messages from the content provider tables
* of the Messaging app, as defined by android.provider.Telephony
*
* Analogous to com.google.android.mms.pdu.PduPersister from
* MMS parsing is analogous to com.google.android.mms.pdu.PduPersister from
* core/java/com/google/android/mms/pdu in the base Android framework
* (https://github.com/android/platform_frameworks_base)
*/
public class MmsUtils
public class MessagingUtils
{
// constants from android.provider.Telephony
public static final Uri OBSERVER_URI = Uri.parse("content://mms-sms/");
public static final Uri INBOX_URI = Uri.parse("content://mms/inbox");
public static final Uri PART_URI = Uri.parse("content://mms/part");
public static final Uri MMS_INBOX_URI = Uri.parse("content://mms/inbox");
public static final Uri MMS_PART_URI = Uri.parse("content://mms/part");
public static final Uri SENT_SMS_URI = Uri.parse("content://sms/sent");
// constants from com.google.android.mms.pdu.PduHeaders
private static final int PDU_HEADER_FROM = 0x89;
private static final int MESSAGE_TYPE_RETRIEVE_CONF = 0x84;
// todo -- prevent unbounded growth?
// todo -- prevent (very slow) unbounded growth?
private final Set<Long> seenMmsIds = new HashSet<Long>();
private final Set<Long> seenSentSmsIds = new HashSet<Long>();
private App app;
private ContentResolver contentResolver;
public MmsUtils(App app)
public MessagingUtils(App app)
{
this.app = app;
this.contentResolver = app.getContentResolver();
}
private List<MmsPart> getMmsParts(long id)
public List<MmsPart> getMmsParts(long id)
{
Cursor cur = contentResolver.query(PART_URI, new String[] {
Cursor cur = contentResolver.query(MMS_PART_URI, new String[] {
"_id", "ct", "name", "text", "cid", "_data"
}, "mid = ?", new String[] { "" + id }, null);
@ -67,7 +70,8 @@ public class MmsUtils
if (name == null || name.length() == 0)
{
name = UUID.randomUUID().toString();
// POST request for incoming MMS will fail if the filename is empty
name = UUID.randomUUID().toString().substring(0, 8);
}
part.setName(name);
@ -90,7 +94,7 @@ public class MmsUtils
/*
* see com.google.android.mms.pdu.PduPersister.loadAddress
*/
private String getSenderNumber(long mmsId) {
public String getMmsSenderNumber(long mmsId) {
Uri uri = Uri.parse("content://mms/"+mmsId+"/addr");
@ -113,43 +117,40 @@ public class MmsUtils
return address;
}
public List<IncomingMms> getMessagesInInbox()
public List<IncomingMms> getMessagesInMmsInbox()
{
return getMessagesInMmsInbox(false);
}
public synchronized List<IncomingMms> getMessagesInMmsInbox(boolean newMessagesOnly)
{
// the M-Retrieve.conf messages are the 'actual' MMS messages
String m_type = "" + MESSAGE_TYPE_RETRIEVE_CONF;
Cursor c = contentResolver.query(INBOX_URI,
Cursor c = contentResolver.query(MMS_INBOX_URI,
new String[] {"_id", "ct_l", "date"},
"m_type = ? ", new String[] { m_type }, null);
"m_type = ? ", new String[] { m_type },
"_id desc limit 30");
List<IncomingMms> messages = new ArrayList<IncomingMms>();
while (c.moveToNext())
{
long id = c.getLong(0);
long date = c.getLong(2);
String from = getSenderNumber(id);
if (from == null)
if (newMessagesOnly && seenMmsIds.contains(id))
{
app.log("Ignoring MMS "+id+" for now because sender number is null");
// avoid fetching all the info for old MMS messages if we're only interested in new ones
continue;
}
long date = c.getLong(2);
IncomingMms mms = new IncomingMms(app,
from,
date * 1000, // MMS timestamp is in seconds for some reason,
// while everything else is in ms
id);
mms.setContentLocation(c.getString(1));
for (MmsPart part : getMmsParts(id))
{
mms.addPart(part);
}
messages.add(mms);
}
c.close();
@ -157,9 +158,9 @@ public class MmsUtils
return messages;
}
public synchronized boolean deleteFromInbox(IncomingMms mms)
public synchronized boolean deleteFromMmsInbox(IncomingMms mms)
{
long id = mms.getId();
long id = mms.getMessagingId();
Uri uri = Uri.parse("content://mms/inbox/" + id);
int res = contentResolver.delete(uri, null, null);
@ -181,15 +182,53 @@ public class MmsUtils
return res > 0;
}
public synchronized void markOldMms(IncomingMms mms)
public synchronized void markSeenMms(IncomingMms mms)
{
long id = mms.getId();
long id = mms.getMessagingId();
seenMmsIds.add(id);
}
public synchronized boolean isNewMms(IncomingMms mms)
public synchronized List<IncomingSms> getSentSmsMessages()
{
long id = mms.getId();
return !seenMmsIds.contains(id);
return getSentSmsMessages(false);
}
public synchronized List<IncomingSms> getSentSmsMessages(boolean newMessagesOnly)
{
Cursor c = contentResolver.query(SENT_SMS_URI,
new String[]{"_id", "address", "body", "date"}, null, null,
"_id desc limit 30");
// SMS messages sent via Messaging app are considered as IncomingSms (with direction=Direction.Sent)
// because they're incoming to the server (whereas OutgoingSms would indicate a message we will try to send)
List<IncomingSms> messages = new ArrayList<IncomingSms>();
while (c.moveToNext())
{
long id = c.getLong(0);
if (newMessagesOnly && seenSentSmsIds.contains(id))
{
continue;
}
IncomingSms sms = new IncomingSms(app);
sms.setMessagingId(id);
sms.setTo(c.getString(1));
sms.setMessageBody(c.getString(2));
sms.setTimestamp(c.getLong(3));
sms.setDirection(IncomingSms.Direction.Sent);
messages.add(sms);
}
c.close();
return messages;
}
public synchronized void markSeenSentSms(IncomingSms sms)
{
long id = sms.getMessagingId();
seenSentSmsIds.add(id);
}
}

View File

@ -1,46 +0,0 @@
package org.envaya.sms;
import android.content.Intent;
import android.database.ContentObserver;
import android.os.Handler;
import java.util.List;
final class MmsObserver extends ContentObserver {
private App app;
public MmsObserver(App app) {
super(new Handler());
this.app = app;
}
public void register()
{
app.getContentResolver().registerContentObserver(
MmsUtils.OBSERVER_URI, true, this);
MmsUtils mmsUtils = app.getMmsUtils();
List<IncomingMms> messages = mmsUtils.getMessagesInInbox();
for (IncomingMms mms : messages)
{
mmsUtils.markOldMms(mms);
}
}
public void unregister()
{
app.getContentResolver().unregisterContentObserver(this);
}
@Override
public void onChange(final boolean selfChange) {
super.onChange(selfChange);
if (!selfChange)
{
// check MMS inbox in an IntentService since it may be slow
// and we only want to do one check at a time
app.startService(new Intent(app, CheckMmsInboxService.class));
}
}
}

View File

@ -6,7 +6,6 @@ import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.SystemClock;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashMap;
@ -18,7 +17,6 @@ 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 {
@ -81,7 +79,7 @@ public class Outbox {
else {
logMessage = "queued";
}
String smsDesc = sms.getLogName();
String smsDesc = sms.getDisplayType();
if (serverId != null) {
app.log("Notifying server " + smsDesc + " " + logMessage);
@ -115,19 +113,21 @@ public class Outbox {
return outgoingMessages.get(uri);
}
public synchronized void messageSent(OutgoingMessage sms)
public synchronized void messageSent(OutgoingMessage message)
{
sms.setProcessingState(OutgoingMessage.ProcessingState.Sent);
message.setProcessingState(OutgoingMessage.ProcessingState.Sent);
sms.clearSendTimeout();
message.clearSendTimeout();
notifyMessageStatus(sms, App.STATUS_SENT, "");
notifyMessageStatus(message, App.STATUS_SENT, "");
Uri uri = sms.getUri();
Uri uri = message.getUri();
outgoingMessages.remove(uri);
addRecentSentMessage(sms);
addRecentSentMessage(message);
app.getDatabaseHelper().deletePendingMessage(message);
notifyChanged();
@ -135,11 +135,11 @@ public class Outbox {
maybeDequeueMessage();
}
private synchronized void addRecentSentMessage(OutgoingMessage sms)
private synchronized void addRecentSentMessage(OutgoingMessage message)
{
if (sms.getServerId() != null)
if (message.getServerId() != null)
{
Uri uri = sms.getUri();
Uri uri = message.getUri();
recentSentMessageUris.add(uri);
recentSentMessageUriOrder.add(uri);
@ -152,20 +152,22 @@ public class Outbox {
}
}
public synchronized void messageFailed(OutgoingMessage sms, String error)
public synchronized void messageFailed(OutgoingMessage message, String error)
{
sms.clearSendTimeout();
message.clearSendTimeout();
if (sms.scheduleRetry())
if (message.scheduleRetry())
{
sms.setProcessingState(OutgoingMessage.ProcessingState.Scheduled);
message.setProcessingState(OutgoingMessage.ProcessingState.Scheduled);
}
else
{
sms.setProcessingState(OutgoingMessage.ProcessingState.None);
message.setProcessingState(OutgoingMessage.ProcessingState.None);
}
notifyChanged();
notifyMessageStatus(sms, App.STATUS_FAILED, error);
notifyMessageStatus(message, App.STATUS_FAILED, error);
app.getDatabaseHelper().deletePendingMessage(message);
numSendingOutgoingMessages--;
maybeDequeueMessage();
@ -185,18 +187,21 @@ public class Outbox {
Uri uri = message.getUri();
if (outgoingMessages.containsKey(uri)) {
app.debug("Duplicate outgoing " + message.getLogName() + ", skipping");
app.debug("Duplicate outgoing " + message.getDisplayType() + ", skipping");
return;
}
if (recentSentMessageUris.contains(uri))
{
app.debug("Outgoing " + message.getLogName() + " already sent, re-notifying server");
app.debug("Outgoing " + message.getDisplayType() + " already sent, re-notifying server");
notifyMessageStatus(message, App.STATUS_SENT, "");
return;
}
outgoingMessages.put(uri, message);
app.getDatabaseHelper().insertPendingMessage(message);
enqueueMessage(message);
}
@ -213,6 +218,8 @@ public class Outbox {
numSendingOutgoingMessages--;
}
app.getDatabaseHelper().deletePendingMessage(message);
notifyMessageStatus(message, App.STATUS_CANCELLED,
"deleted by user");
app.log(message.getDescription() + " deleted");

View File

@ -48,6 +48,11 @@ public abstract class OutgoingMessage extends QueuedMessage {
return state;
}
public static OutgoingMessage newFromMessageType(App app, String type)
{
return new OutgoingSms(app);
}
public void setProcessingState(ProcessingState status)
{
this.state = status;
@ -157,12 +162,11 @@ public abstract class OutgoingMessage extends QueuedMessage {
return getDisplayType() + " to " + getTo();
}
abstract String getLogName();
public void validate() throws ValidationException
{
}
abstract String getMessageType();
abstract ScheduleInfo scheduleSend();
abstract void send(ScheduleInfo schedule);

View File

@ -11,9 +11,9 @@ public class OutgoingSms extends OutgoingMessage {
super(app);
}
public String getLogName()
public String getMessageType()
{
return "SMS";
return App.MESSAGE_TYPE_SMS;
}
private ArrayList<String> _bodyParts;
@ -65,7 +65,7 @@ public class OutgoingSms extends OutgoingMessage {
if (numRetries == 0)
{
app.log("Sending " + getDescription());
app.log("Sending " + getDisplayType());
}
else
{
@ -105,9 +105,16 @@ public class OutgoingSms extends OutgoingMessage {
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");
if (app.isTestMode() && app.autoAddTestNumber())
{
app.addTestPhoneNumber(to);
}
else
{
// 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();

View File

@ -15,11 +15,28 @@ public abstract class QueuedMessage
protected Date dateCreated = new Date();
public App app;
protected long persistedId = 0; // _id of row in pending_incoming_messages or pending_outgoing_messages table (0 if not stored)
public QueuedMessage(App app)
{
this.app = app;
}
public boolean isPersisted()
{
return persistedId != 0;
}
public long getPersistedId()
{
return persistedId;
}
public void setPersistedId(long id)
{
this.persistedId = id;
}
public Date getDateCreated()
{
return dateCreated;

View File

@ -1,11 +1,18 @@
package org.envaya.sms;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Iterator;
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.json.JSONException;
import org.json.JSONObject;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
@ -38,4 +45,53 @@ public class XmlUtils {
}
return null;
}
public static List<OutgoingMessage> getMessagesList(Document xml, App app, String defaultTo)
{
List<OutgoingMessage> messages = new ArrayList<OutgoingMessage>();
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++)
{
Element messageElement = (Element) messageNodes.item(i);
String nodeName = messageElement.getNodeName();
OutgoingMessage message = OutgoingMessage.newFromMessageType(app, nodeName);
message.setFrom(app.getPhoneNumber());
String to = messageElement.getAttribute("to");
message.setTo("".equals(to) ? defaultTo : to);
String serverId = messageElement.getAttribute("id");
message.setServerId("".equals(serverId) ? 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);
}
}
return messages;
}
}

View File

@ -1,15 +0,0 @@
package org.envaya.sms.receiver;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
public class BootReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent)
{
// just want to initialize App class to start outgoing message poll timer
}
}

View File

@ -14,6 +14,8 @@ public class ExpansionPackInstallReceiver extends BroadcastReceiver
String action = intent.getAction();
boolean isInstalled = !"android.intent.action.PACKAGE_REMOVED".equals(action);
String packageName = intent.getData().getSchemeSpecificPart();
if (packageName != null)

View File

@ -0,0 +1,22 @@
package org.envaya.sms.receiver;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import org.envaya.sms.App;
public class NudgeReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent)
{
// intentional side-effect: initialize App class to start outgoing message poll timer,
// and send any pending incoming messages that were persisted to DB before reboot.
//App app = (App)context.getApplicationContext();
//app.debug("Nudged by " + intent.getAction());
//app.log(".");
}
}

View File

@ -0,0 +1,22 @@
package org.envaya.sms.receiver;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import org.envaya.sms.App;
public class StartAmqpConsumer extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
final App app = (App) context.getApplicationContext();
if (!app.isEnabled())
{
return;
}
app.getAmqpConsumer().startAsync();
}
}

View File

@ -0,0 +1,44 @@
package org.envaya.sms.service;
import android.app.IntentService;
import android.content.Intent;
import org.envaya.sms.AmqpConsumer;
import org.envaya.sms.App;
public class AmqpConsumerService extends IntentService {
private App app;
public AmqpConsumerService(String name)
{
super(name);
}
public AmqpConsumerService()
{
this("AmqpConsumerService");
}
@Override
public void onCreate() {
super.onCreate();
app = (App)this.getApplicationContext();
}
@Override
protected void onHandleIntent(Intent intent)
{
boolean start = intent.getBooleanExtra("start", false);
AmqpConsumer consumer = app.getAmqpConsumer();
if (start)
{
consumer.startBlocking();
}
else
{
consumer.stopBlocking();
}
}
}

View File

@ -0,0 +1,34 @@
package org.envaya.sms.service;
import android.app.IntentService;
import android.content.Intent;
import org.envaya.sms.AmqpConsumer;
import org.envaya.sms.App;
public class AmqpHeartbeatService extends IntentService {
private App app;
public AmqpHeartbeatService(String name)
{
super(name);
}
public AmqpHeartbeatService()
{
this("AmqpHeartbeatService");
}
@Override
public void onCreate() {
super.onCreate();
app = (App)this.getApplicationContext();
}
@Override
protected void onHandleIntent(Intent intent)
{
AmqpConsumer consumer = app.getAmqpConsumer();
consumer.sendHeartbeatBlocking();
}
}

View File

@ -0,0 +1,91 @@
package org.envaya.sms.service;
import android.app.IntentService;
import android.content.Intent;
import org.envaya.sms.App;
import org.envaya.sms.IncomingMms;
import org.envaya.sms.IncomingSms;
import org.envaya.sms.MessagingUtils;
import java.util.List;
public class CheckMessagingService extends IntentService
{
private App app;
private MessagingUtils messagingUtils;
public CheckMessagingService(String name)
{
super(name);
}
public CheckMessagingService()
{
this("CheckMessagingService");
}
@Override
public void onCreate() {
super.onCreate();
app = (App)this.getApplicationContext();
messagingUtils = app.getMessagingUtils();
}
@Override
protected void onHandleIntent(Intent intent)
{
checkNewSentSms();
checkNewMms();
}
private void checkNewSentSms()
{
List<IncomingSms> messages = messagingUtils.getSentSmsMessages(true);
for (IncomingSms sms : messages)
{
messagingUtils.markSeenSentSms(sms);
if (sms.isForwardable())
{
app.log("SMS id=" + sms.getMessagingId() + " sent via Messaging app");
app.inbox.forwardMessage(sms);
}
else
{
app.log("Ignoring SMS sent via Messaging app");
}
}
}
private void checkNewMms()
{
List<IncomingMms> messages = messagingUtils.getMessagesInMmsInbox(true);
for (IncomingMms mms : messages)
{
String from = mms.getFrom();
if (from == null || from.length() == 0)
{
// sender phone number may not be written to Messaging database yet
continue;
}
// prevent forwarding MMS messages that existed in inbox
// before EnvayaSMS started, or re-forwarding MMS multiple
// times if we don't delete them.
messagingUtils.markSeenMms(mms);
if (mms.isForwardable())
{
app.log("New MMS id=" + mms.getMessagingId() + " in inbox");
app.inbox.forwardMessage(mms);
}
else
{
app.log("Ignoring incoming MMS from " + mms.getFrom());
}
}
}
}

View File

@ -0,0 +1,66 @@
package org.envaya.sms.service;
import android.app.AlarmManager;
import android.app.IntentService;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.telephony.PhoneStateListener;
import android.telephony.TelephonyManager;
import org.envaya.sms.App;
import org.envaya.sms.receiver.NudgeReceiver;
public class EnabledChangedService extends IntentService {
private App app;
public EnabledChangedService(String name)
{
super(name);
}
public EnabledChangedService()
{
this("EnabledChangedService");
}
@Override
public void onCreate() {
super.onCreate();
app = (App)this.getApplicationContext();
}
@Override
protected void onHandleIntent(Intent intent)
{
TelephonyManager telephony = (TelephonyManager)
getSystemService(Context.TELEPHONY_SERVICE);
startService(new Intent(app, ForegroundService.class));
app.setOutgoingMessageAlarm();
AlarmManager alarmManager = (AlarmManager) app.getSystemService(Context.ALARM_SERVICE);
alarmManager.cancel(PendingIntent.getBroadcast(app, 0, new Intent(app, NudgeReceiver.class), 0));
if (app.isEnabled())
{
app.getMessagingObserver().register();
telephony.listen(app.getCallListener(), PhoneStateListener.LISTEN_CALL_STATE);
app.getDatabaseHelper().restorePendingMessages();
app.getAmqpConsumer().startAsync();
}
else
{
app.getMessagingObserver().unregister();
telephony.listen(app.getCallListener(), PhoneStateListener.LISTEN_NONE);
app.getAmqpConsumer().stopAsync();
}
}
}

View File

@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.envaya.sms;
package org.envaya.sms.service;
import android.app.Notification;
import android.app.NotificationManager;
@ -23,10 +23,12 @@ import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
import android.util.Log;
import org.envaya.sms.App;
import org.envaya.sms.R;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import org.envaya.sms.ui.LogView;
import org.envaya.sms.ui.Main;
/*
* Service running in foreground to make sure App instance stays
@ -159,11 +161,10 @@ public class ForegroundService extends Service {
System.currentTimeMillis());
PendingIntent contentIntent = PendingIntent.getActivity(this, 0,
new Intent(this, LogView.class), 0);
new Intent(this, Main.class), 0);
notification.setLatestEventInfo(this,
"EnvayaSMS running",
text, contentIntent);
CharSequence info = getText(R.string.running);
notification.setLatestEventInfo(this, info, text, contentIntent);
startForegroundCompat(R.string.service_started, notification);
}

View File

@ -3,6 +3,8 @@ package org.envaya.sms.task;
import android.os.AsyncTask;
import android.os.Build;
import org.envaya.sms.App;
import org.envaya.sms.JsonUtils;
import org.envaya.sms.XmlUtils;
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.ArrayList;
@ -17,6 +19,9 @@ 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.json.JSONObject;
import org.w3c.dom.Document;
import org.envaya.sms.R;
public class BaseHttpTask extends AsyncTask<String, Void, HttpResponse> {
@ -34,6 +39,8 @@ public class BaseHttpTask extends AsyncTask<String, Void, HttpResponse> {
this.url = url;
this.app = app;
params = new ArrayList<BasicNameValuePair>(Arrays.asList(paramsArr));
params.add(new BasicNameValuePair("version", "" + app.getPackageInfo().versionCode));
}
public void addParam(String name, String value)
@ -51,7 +58,7 @@ public class BaseHttpTask extends AsyncTask<String, Void, HttpResponse> {
{
HttpPost httpPost = new HttpPost(url);
httpPost.setHeader("User-Agent", "EnvayaSMS/" + app.getPackageInfo().versionName + " (Android; SDK "+Build.VERSION.SDK_INT + "; " + Build.MANUFACTURER + "; " + Build.MODEL+")");
httpPost.setHeader("User-Agent", app.getText(R.string.app_name) + "/" + app.getPackageInfo().versionName + " (Android; SDK "+Build.VERSION.SDK_INT + "; " + Build.MANUFACTURER + "; " + Build.MODEL+")");
if (useMultipartPost)
{
@ -83,6 +90,7 @@ public class BaseHttpTask extends AsyncTask<String, Void, HttpResponse> {
try
{
post = makeHttpPost();
HttpClient client = app.getHttpClient();
return client.execute(post);
}
@ -97,9 +105,10 @@ public class BaseHttpTask extends AsyncTask<String, Void, HttpResponse> {
if ((ex instanceof IOException)
&& message != null && message.equals("Connection already shutdown"))
{
//app.log("Retrying request");
// app.log("Retrying request");
post = makeHttpPost();
HttpClient client = app.getHttpClient();
return client.execute(post);
}
}
@ -112,9 +121,34 @@ public class BaseHttpTask extends AsyncTask<String, Void, HttpResponse> {
return null;
}
public boolean isValidContentType(String contentType)
protected String getErrorText(HttpResponse response)
throws Exception
{
return true; // contentType.startsWith("text/xml");
String contentType = getContentType(response);
String error = null;
if (contentType.startsWith("application/json"))
{
JSONObject json = JsonUtils.parseResponse(response);
error = JsonUtils.getErrorText(json);
}
else if (contentType.startsWith("text/xml"))
{
Document xml = XmlUtils.parseResponse(response);
error = XmlUtils.getErrorText(xml);
}
if (error == null)
{
error = "HTTP " + response.getStatusLine().getStatusCode();
}
return error;
}
protected String getContentType(HttpResponse response)
{
Header contentTypeHeader = response.getFirstHeader("Content-Type");
return (contentTypeHeader != null) ? contentTypeHeader.getValue() : "";
}
@Override
@ -124,23 +158,12 @@ public class BaseHttpTask extends AsyncTask<String, Void, HttpResponse> {
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);
}
handleResponse(response);
}
else if (statusCode >= 400 && statusCode <= 499 && validContentType)
else if (statusCode >= 400 && statusCode <= 499)
{
handleErrorResponse(response);
handleFailure();
@ -174,11 +197,6 @@ public class BaseHttpTask extends AsyncTask<String, Void, HttpResponse> {
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

View File

@ -31,11 +31,17 @@ public class CheckConnectivityTask extends AsyncTask<String, Void, Boolean> {
{
try
{
Thread.sleep(1000);
InetAddress addr = InetAddress.getByName(hostName);
if (addr.isReachable(App.HTTP_CONNECTION_TIMEOUT))
{
return true;
}
}
catch (InterruptedException ex)
{
}
catch (IOException ex)
{

View File

@ -3,7 +3,6 @@ package org.envaya.sms.task;
import org.apache.http.HttpResponse;
import org.apache.http.message.BasicNameValuePair;
import org.envaya.sms.IncomingMessage;
import org.envaya.sms.OutgoingMessage;
public class ForwarderTask extends HttpTask {
@ -14,12 +13,6 @@ public class ForwarderTask extends HttpTask {
this.message = message;
}
@Override
public boolean isValidContentType(String contentType)
{
return contentType.startsWith("text/xml");
}
@Override
protected String getDefaultToAddress() {
return message.getFrom();
@ -27,12 +20,7 @@ public class ForwarderTask extends HttpTask {
@Override
protected void handleResponse(HttpResponse response) throws Exception {
for (OutgoingMessage reply : parseResponseXML(response)) {
app.outbox.sendMessage(reply);
}
app.inbox.messageForwarded(message);
super.handleResponse(response);
}

View File

@ -4,32 +4,31 @@
*/
package org.envaya.sms.task;
import org.envaya.sms.XmlUtils;
import org.envaya.sms.OutgoingSms;
import org.envaya.sms.OutgoingMessage;
import org.envaya.sms.App;
import org.envaya.sms.JsonUtils;
import org.envaya.sms.Base64Coder;
import android.content.SharedPreferences;
import org.envaya.sms.App;
import org.envaya.sms.XmlUtils;
import android.content.Intent;
import android.content.IntentFilter;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.preference.PreferenceManager;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.SocketTimeoutException;
import java.net.UnknownHostException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import javax.xml.parsers.ParserConfigurationException;
import org.apache.commons.io.IOUtils;
import org.apache.http.HttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.message.BasicNameValuePair;
import org.json.JSONException;
import org.json.JSONObject;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
public class HttpTask extends BaseHttpTask {
@ -99,9 +98,35 @@ public class HttpTask extends BaseHttpTask {
logEntries = app.getNewLogEntries();
params.add(new BasicNameValuePair("version", "" + app.getPackageInfo().versionCode));
params.add(new BasicNameValuePair("phone_number", app.getPhoneNumber()));
params.add(new BasicNameValuePair("phone_id", app.getPhoneID()));
params.add(new BasicNameValuePair("phone_token", app.getPhoneToken()));
params.add(new BasicNameValuePair("send_limit", "" + app.getOutgoingMessageLimit()));
params.add(new BasicNameValuePair("now", "" + System.currentTimeMillis()));
params.add(new BasicNameValuePair("settings_version", "" + app.getSettingsVersion()));
Intent lastBatteryIntent = app.registerReceiver(null, new IntentFilter(Intent.ACTION_BATTERY_CHANGED));
if (lastBatteryIntent != null)
{
// BatteryManager.EXTRA_* constants introduced in API level 5 (2.0)
int rawLevel = lastBatteryIntent.getIntExtra("level", -1);
int scale = lastBatteryIntent.getIntExtra("scale", -1);
int pctLevel = (rawLevel > 0 && scale > 0) ? (rawLevel * 100 / scale) : rawLevel;
if (pctLevel >= 0)
{
params.add(new BasicNameValuePair("battery", "" + pctLevel));
}
int plugged = lastBatteryIntent.getIntExtra("plugged", -1);
if (plugged >= 0)
{
params.add(new BasicNameValuePair("power", "" + plugged));
}
}
ConnectivityManager cm =
(ConnectivityManager)app.getSystemService(App.CONNECTIVITY_SERVICE);
@ -135,53 +160,26 @@ public class HttpTask extends BaseHttpTask {
return "";
}
protected List<OutgoingMessage> parseResponseXML(HttpResponse response)
protected void handleResponseJSON(JSONObject json)
throws JSONException
{
JsonUtils.processEvents(json, app, getDefaultToAddress());
}
protected void handleResponseXML(Document xml)
throws IOException, ParserConfigurationException, SAXException
{
List<OutgoingMessage> messages = new ArrayList<OutgoingMessage>();
Document xml = XmlUtils.parseResponse(response);
Element messagesElement = (Element) xml.getElementsByTagName("messages").item(0);
if (messagesElement != null)
for (OutgoingMessage message : XmlUtils.getMessagesList(xml, app, getDefaultToAddress()))
{
NodeList messageNodes = messagesElement.getChildNodes();
int numNodes = messageNodes.getLength();
for (int i = 0; i < numNodes; i++)
{
Element messageElement = (Element) messageNodes.item(i);
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);
}
app.outbox.sendMessage(message);
}
return messages;
}
protected void handleUnknownContentType(String contentType)
throws Exception
{
// old server API only mandated valid content type for action=outgoing
app.log("Warning: Unknown response type " + contentType);
}
@Override
@ -222,15 +220,37 @@ public class HttpTask extends BaseHttpTask {
@Override
public void handleErrorResponse(HttpResponse response) throws Exception
{
Document xml = XmlUtils.parseResponse(response);
String error = XmlUtils.getErrorText(xml);
if (error != null)
app.log(getErrorText(response));
}
@Override
protected void handleResponse(HttpResponse response) throws Exception {
String contentType = getContentType(response);
if (contentType.startsWith("application/json"))
{
app.log(error);
String responseBody = IOUtils.toString(response.getEntity().getContent(), "UTF-8");
JSONObject json = new JSONObject(responseBody);
handleResponseJSON(json);
}
else if (contentType.startsWith("text/xml"))
{
Document xml = XmlUtils.parseResponse(response);
handleResponseXML(xml);
}
else
{
app.log("HTTP " +response.getStatusLine().getStatusCode());
handleUnknownContentType(contentType);
}
// if we get a valid server response after a connectivity error, then forward any pending messages
if (app.hasConnectivityError())
{
app.onConnectivityRestored();
}
}
}

View File

@ -4,7 +4,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.OutgoingMessage;
public class PollerTask extends HttpTask {
@ -12,12 +11,6 @@ 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);
@ -25,9 +18,9 @@ public class PollerTask extends HttpTask {
}
@Override
protected void handleResponse(HttpResponse response) throws Exception {
for (OutgoingMessage reply : parseResponseXML(response)) {
app.outbox.sendMessage(reply);
}
protected void handleUnknownContentType(String contentType)
throws Exception
{
throw new Exception("Invalid response type " + contentType);
}
}

View File

@ -0,0 +1,81 @@
package org.envaya.sms.ui;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Bundle;
import android.preference.Preference;
import android.preference.PreferenceActivity;
import android.preference.PreferenceScreen;
import android.view.Menu;
import org.envaya.sms.App;
import org.envaya.sms.R;
public class ExpansionPacks extends PreferenceActivity {
private App app;
private BroadcastReceiver installReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
updateInstallStatus();
}
};
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
addPreferencesFromResource(R.xml.expansion_packs);
app = (App) getApplication();
IntentFilter installReceiverFilter = new IntentFilter();
installReceiverFilter.addAction(App.EXPANSION_PACKS_CHANGED_INTENT);
registerReceiver(installReceiver, installReceiverFilter);
updateInstallStatus();
}
@Override
public void onDestroy()
{
unregisterReceiver(installReceiver);
super.onDestroy();
}
public void updateInstallStatus()
{
PreferenceScreen screen = this.getPreferenceScreen();
int numPrefs = screen.getPreferenceCount();
String basePackageName = app.getPackageName();
this.setTitle("Telerivet : SMS Rate Limit ("+app.getOutgoingMessageLimit()+")");
for(int i=0; i < numPrefs;i++)
{
Preference p = screen.getPreference(i);
String packageNum = p.getKey();
String packageName = basePackageName + "." + packageNum;
if (app.isSmsExpansionPackInstalled(packageName))
{
p.setSummary("Installed.");
}
else
{
p.setSummary("Not installed.\nInstall to increase limit by 100 SMS/hour...");
}
}
}
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
this.finish();
return true;
}
}

View File

@ -27,7 +27,7 @@ public class Help extends Activity {
app = (App)getApplication();
String html = "<b>EnvayaSMS " + app.getPackageInfo().versionName + "</b><br /><br />"
String html = "<b>"+getText(R.string.app_name)+" " + app.getPackageInfo().versionName + "</b><br /><br />"
+ "Menu icons cc/by www.androidicons.com<br /><br />";
help.setText(Html.fromHtml(html));

View File

@ -2,21 +2,28 @@ package org.envaya.sms.ui;
import org.envaya.sms.task.HttpTask;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.*;
import android.content.DialogInterface.OnCancelListener;
import android.content.DialogInterface.OnClickListener;
import android.net.Uri;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.text.Html;
import android.text.method.LinkMovementMethod;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.widget.Button;
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;
import java.util.ArrayList;
import java.util.List;
public class LogView extends Activity {
@ -29,8 +36,25 @@ public class LogView extends Activity {
}
};
private BroadcastReceiver settingsReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
updateUpgradeButton();
updateInfo();
}
};
private BroadcastReceiver expansionPacksReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
updateInfo();
}
};
private ScrollView scrollView;
private TextView info;
private TextView log;
private TextView heading;
private class TestTask extends HttpTask
{
@ -47,14 +71,31 @@ public class LogView extends Activity {
private int lastLogEpoch = -1;
public void updateLogView()
public void updateUpgradeButton()
{
Button upgradeButton = (Button) this.findViewById(R.id.upgrade_button);
boolean isUpgradeAvailable = app.isUpgradeAvailable();
if (isUpgradeAvailable)
{
upgradeButton.setText("New version of app available ("+app.getMarketVersionName()+").\nClick to install...");
upgradeButton.setVisibility(View.VISIBLE);
}
else
{
upgradeButton.setVisibility(View.GONE);
}
}
public synchronized void updateLogView()
{
int logEpoch = app.getLogEpoch();
CharSequence displayedLog = app.getDisplayedLog();
int logEpoch2 = app.getLogEpoch();
if (lastLogEpoch == logEpoch)
if (lastLogEpoch == logEpoch && logEpoch == logEpoch2)
{
int beforeLen = info.getText().length();
int beforeLen = log.getText().length();
int afterLen = displayedLog.length();
if (beforeLen == afterLen)
@ -62,11 +103,11 @@ public class LogView extends Activity {
return;
}
info.append(displayedLog, beforeLen, afterLen);
log.append(displayedLog, beforeLen, afterLen);
}
else
{
info.setText(displayedLog);
log.setText(displayedLog);
lastLogEpoch = logEpoch;
}
@ -85,24 +126,227 @@ public class LogView extends Activity {
setContentView(R.layout.log_view);
PreferenceManager.setDefaultValues(this, R.xml.prefs, false);
scrollView = (ScrollView) this.findViewById(R.id.info_scroll);
scrollView = (ScrollView) this.findViewById(R.id.log_scroll);
heading = (TextView) this.findViewById(R.id.heading);
info = (TextView) this.findViewById(R.id.info);
info.setMovementMethod(LinkMovementMethod.getInstance());
updateInfo();
//info.setMovementMethod(new ScrollingMovementMethod());
log = (TextView) this.findViewById(R.id.log);
log.setMovementMethod(LinkMovementMethod.getInstance());
updateUpgradeButton();
updateLogView();
IntentFilter logReceiverFilter = new IntentFilter();
logReceiverFilter.addAction(App.LOG_CHANGED_INTENT);
registerReceiver(logReceiver, logReceiverFilter);
registerReceiver(logReceiver, new IntentFilter(App.LOG_CHANGED_INTENT));
registerReceiver(settingsReceiver, new IntentFilter(App.SETTINGS_CHANGED_INTENT));
registerReceiver(expansionPacksReceiver, new IntentFilter(App.EXPANSION_PACKS_CHANGED_INTENT));
if (savedInstanceState == null)
{
if (getIntent().getBooleanExtra("configured", false))
{
showConfigureSuccessDialog();
}
else if (app.isUpgradeAvailable())
{
showUpgradeDialog();
}
}
else
{
curDialog = savedInstanceState.getInt("cur_dialog", 0);
if (curDialog == UPGRADE_DIALOG)
{
showUpgradeDialog();
}
else if (curDialog == CONFIGURE_SUCCESS_DIALOG)
{
showConfigureSuccessDialog();
}
else if (curDialog == SETTINGS_DIALOG)
{
showSettingsDialog();
}
}
}
public static final int NO_DIALOG = 0;
public static final int UPGRADE_DIALOG = 1;
public static final int CONFIGURE_SUCCESS_DIALOG = 2;
public static final int SETTINGS_DIALOG = 3;
private int curDialog = NO_DIALOG;
public void updateInfo()
{
boolean enabled = app.isEnabled();
heading.setText(Html.fromHtml(
enabled ? "<b>" + getText(R.string.running) + " ("+app.getPhoneNumber()+")</b>"
: "<b>" +getText(R.string.disabled) + "</b>"));
if (enabled)
{
info.setText("New messages will be forwarded to server");
if (app.isTestMode())
{
info.append("\n(Test mode enabled)");
}
}
else
{
info.setText("New messages will not be forwarded to server");
}
}
public void infoClicked(View v)
{
startActivity(new Intent(this, Prefs.class));
}
@Override
protected void onSaveInstanceState(Bundle outState)
{
super.onSaveInstanceState(outState);
outState.putInt("cur_dialog", curDialog);
}
public void showUpgradeDialog()
{
curDialog = UPGRADE_DIALOG;
new AlertDialog.Builder(this)
.setTitle("Upgrade available")
.setMessage("A new version of the app is available ("+app.getMarketVersionName()+"). Do you want to upgrade now?")
.setPositiveButton("OK", new OnClickListener() {
public void onClick(DialogInterface dialog, int i)
{
upgradeClicked(null);
}
})
.setNegativeButton("Not Now", new DismissDialogListener())
.setOnCancelListener(new DismissDialogListener())
.setCancelable(true)
.show();
}
public void showConfigureSuccessDialog()
{
curDialog = CONFIGURE_SUCCESS_DIALOG;
new AlertDialog.Builder(this)
.setTitle("App configured successfully!")
.setMessage("Now try using another mobile phone to send a SMS to this phone.\n\nYou should be able to see the message on your Messages page on telerivet.com, and send replies.")
.setPositiveButton("OK", new OnClickListener() {
public void onClick(DialogInterface dialog, int i)
{
showSettingsDialog();
}
})
.setOnCancelListener(new DismissDialogListener())
.setCancelable(true)
.show();
}
public String getSettingsSummary()
{
StringBuilder builder = new StringBuilder();
if (app.getKeepInInbox())
{
builder.append("- New messages kept in Messaging inbox\n");
}
else
{
builder.append("- New messages not kept in Messaging inbox\n");
}
if (app.callNotificationsEnabled())
{
builder.append("- Call notifications enabled\n");
}
else
{
builder.append("- Call notifications disabled\n");
}
List<String> ignoredNumbers = app.getIgnoredPhoneNumbers();
boolean ignoreShortcodes = app.ignoreShortcodes();
boolean ignoreNonNumeric = app.ignoreNonNumeric();
boolean testMode = app.isTestMode();
builder.append("- Send up to " + app.getOutgoingMessageLimit()+ " SMS/hour\n");
if (ignoredNumbers.isEmpty() && !ignoreShortcodes && !ignoreNonNumeric && !testMode)
{
builder.append("- Forward messages from all phone numbers");
}
else if (testMode)
{
builder.append("- Forward messages only from certain phone numbers");
}
else
{
builder.append("- Ignore messages from some phone numbers");
}
return builder.toString();
}
public void showSettingsDialog()
{
curDialog = SETTINGS_DIALOG;
new AlertDialog.Builder(this)
.setTitle("Verify Settings")
.setMessage(getSettingsSummary())
.setPositiveButton("OK", new DismissDialogListener())
.setNegativeButton("Change", new OnClickListener() {
public void onClick(DialogInterface dialog, int i)
{
curDialog = NO_DIALOG;
startActivity(new Intent(LogView.this, Prefs.class));
}
})
.setOnCancelListener(new DismissDialogListener())
.setCancelable(true)
.show();
}
public class DismissDialogListener implements OnClickListener, OnCancelListener
{
public void onCancel(DialogInterface dialog)
{
curDialog = NO_DIALOG;
dialog.dismiss();
}
public void onClick(DialogInterface dialog, int i)
{
curDialog = NO_DIALOG;
dialog.dismiss();
}
}
public void upgradeClicked(View v)
{
startActivity(new Intent(Intent.ACTION_VIEW,
Uri.parse("market://details?id=" + app.getPackageInfo().applicationInfo.packageName)));
}
@Override
public void onDestroy()
{
this.unregisterReceiver(logReceiver);
unregisterReceiver(logReceiver);
unregisterReceiver(settingsReceiver);
unregisterReceiver(expansionPacksReceiver);
super.onDestroy();
}
@ -119,8 +363,8 @@ public class LogView extends Activity {
case R.id.retry_now:
app.retryStuckMessages();
return true;
case R.id.forward_inbox:
startActivity(new Intent(this, MessagingInbox.class));
case R.id.forward_saved:
startActivity(new Intent(this, MessagingSmsInbox.class));
return true;
case R.id.pending:
startActivity(new Intent(this, PendingMessages.class));

View File

@ -0,0 +1,23 @@
package org.envaya.sms.ui;
import org.envaya.sms.ui.Prefs;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import org.envaya.sms.App;
import org.envaya.sms.ui.LogView;
public class Main extends Activity {
private App app;
/** Called when the activity is first created. */
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
app = (App)getApplication();
startActivity(new Intent(this, LogView.class));
}
}

View File

@ -5,29 +5,28 @@ import android.app.AlertDialog;
import android.app.ListActivity;
import android.content.DialogInterface;
import android.content.DialogInterface.OnClickListener;
import android.database.Cursor;
import android.net.Uri;
import android.content.Intent;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.widget.AdapterView;
import android.widget.*;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.ListView;
import android.widget.SimpleCursorAdapter;
import android.widget.Toast;
import android.widget.AdapterView.OnItemSelectedListener;
import org.envaya.sms.App;
import org.envaya.sms.IncomingMessage;
import org.envaya.sms.IncomingSms;
import org.envaya.sms.R;
import java.util.Arrays;
public class MessagingInbox extends ListActivity {
public abstract class MessagingForwarder extends ListActivity {
private App app;
protected App app;
private Cursor cur;
abstract int getMessageCount();
abstract IncomingMessage getMessageAtPosition(int position);
abstract void initListAdapter();
/** Called when the activity is first created. */
@Override
@ -38,22 +37,57 @@ public class MessagingInbox extends ListActivity {
setContentView(R.layout.inbox);
// undocumented API; see
// core/java/android/provider/Telephony.java
final String[] inboxTypeClasses = new String[] {
"org.envaya.sms.ui.MessagingSmsInbox",
"org.envaya.sms.ui.MessagingMmsInbox",
"org.envaya.sms.ui.MessagingSentSms",
};
Uri inboxUri = Uri.parse("content://sms/inbox");
final String[] inboxTypeNames = new String[] {
"SMS Inbox",
"MMS Inbox",
"Sent SMS"
};
cur = getContentResolver().query(inboxUri,
new String[] { "_id", "address", "body", "date" }, null, null,
"_id desc limit 50");
Spinner spinner = (Spinner) findViewById(R.id.inbox_selector);
SimpleCursorAdapter adapter = new SimpleCursorAdapter(this,
R.layout.inbox_item,
cur,
new String[] {"address","body"},
new int[] {R.id.inbox_address, R.id.inbox_body});
ArrayAdapter<String> inboxTypeAdapter = new ArrayAdapter<String>(this,
android.R.layout.simple_spinner_item, inboxTypeNames);
inboxTypeAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
spinner.setAdapter(inboxTypeAdapter);
setListAdapter(adapter);
final String className = this.getClass().getCanonicalName();
int classIndex = Arrays.asList(inboxTypeClasses).indexOf(className);
if (classIndex != -1)
{
spinner.setSelection(classIndex);
}
spinner.setOnItemSelectedListener(new OnItemSelectedListener() {
public void onItemSelected(AdapterView<?> parent,
View view, int pos, long id) {
String cls = inboxTypeClasses[pos];
if (!className.equals(cls))
{
try
{
finish();
startActivity(new Intent(app, Class.forName(cls)));
}
catch (ClassNotFoundException ex)
{
app.logError(ex);
}
}
}
public void onNothingSelected(AdapterView parent) {
}
});
initListAdapter();
ListView listView = getListView();
@ -63,9 +97,9 @@ public class MessagingInbox extends ListActivity {
{
final IncomingMessage message = getMessageAtPosition(position);
final CharSequence[] options = {"Forward", "Cancel"};
final CharSequence[] options = {"Forward to server", "Cancel"};
new AlertDialog.Builder(MessagingInbox.this)
new AlertDialog.Builder(MessagingForwarder.this)
.setTitle(message.getDescription())
.setItems(options, new OnClickListener() {
public void onClick(DialogInterface dialog, int which)
@ -73,7 +107,7 @@ public class MessagingInbox extends ListActivity {
if (which == 0)
{
app.inbox.forwardMessage(message);
showToast("Forwarding " + message.getDescription());
showToast("Forwarding " + message.getDescription() + " to server");
}
dialog.dismiss();
}
@ -88,23 +122,8 @@ public class MessagingInbox extends ListActivity {
Toast.makeText(this, text, Toast.LENGTH_SHORT).show();
}
public IncomingMessage getMessageAtPosition(int position)
{
int addressIndex = cur.getColumnIndex("address");
int bodyIndex = cur.getColumnIndex("body");
int dateIndex = cur.getColumnIndex("date");
cur.moveToPosition(position);
String address = cur.getString(addressIndex);
String body = cur.getString(bodyIndex);
long date = cur.getLong(dateIndex);
return new IncomingSms(app, address, body, date);
}
public void forwardAllClicked() {
final int count = cur.getCount();
final int count = getMessageCount();
for (int i = 0; i < count; ++i)
{
@ -137,9 +156,9 @@ public class MessagingInbox extends ListActivity {
public boolean onPrepareOptionsMenu(Menu menu) {
MenuItem forwardItem = menu.findItem(R.id.forward_all);
int numMessages = cur.getCount();
int numMessages = getMessageCount();
forwardItem.setEnabled(numMessages > 0);
forwardItem.setTitle("Forward All (" + numMessages + ")");
forwardItem.setTitle("Forward all to server (" + numMessages + ")");
return true;
}

View File

@ -0,0 +1,67 @@
package org.envaya.sms.ui;
import org.envaya.sms.IncomingMms;
import org.envaya.sms.IncomingMessage;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.TextView;
import org.envaya.sms.R;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
public class MessagingMmsInbox extends MessagingForwarder
{
private List<IncomingMms> messages;
public IncomingMessage getMessageAtPosition(int position)
{
return messages.get(position);
}
public int getMessageCount()
{
return messages.size();
}
public void initListAdapter() {
// undocumented API; see
// core/java/android/provider/Telephony.java
messages = app.getMessagingUtils().getMessagesInMmsInbox();
final LayoutInflater inflater = (LayoutInflater)getSystemService(Context.LAYOUT_INFLATER_SERVICE);
final DateFormat dateFormat = new SimpleDateFormat("dd MMM HH:mm:ss");
ArrayAdapter<IncomingMms> arrayAdapter = new ArrayAdapter<IncomingMms>(this,
R.layout.inbox_item,
messages.toArray(new IncomingMms[]{})) {
@Override
public View getView(int position, View convertView, ViewGroup parent) {
View v = convertView;
if (v == null) {
v = inflater.inflate(R.layout.inbox_item, null);
}
IncomingMms mms = messages.get(position);
if (mms == null)
{
return null;
}
TextView addrText = (TextView) v.findViewById(R.id.inbox_address);
TextView bodyText = (TextView) v.findViewById(R.id.inbox_body);
addrText.setText(mms.getFrom() + " (" + dateFormat.format(new Date(mms.getTimestamp())) + ")");
bodyText.setText(mms.getMessageBody());
return v;
}
};
setListAdapter(arrayAdapter);
}
}

View File

@ -0,0 +1,79 @@
package org.envaya.sms.ui;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CursorAdapter;
import android.widget.TextView;
import org.envaya.sms.IncomingMessage;
import org.envaya.sms.IncomingSms;
import org.envaya.sms.R;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
public class MessagingSentSms extends MessagingForwarder {
private Cursor cur;
public IncomingMessage getMessageAtPosition(int position)
{
int addressIndex = cur.getColumnIndex("address");
int bodyIndex = cur.getColumnIndex("body");
int dateIndex = cur.getColumnIndex("date");
cur.moveToPosition(position);
IncomingSms sms = new IncomingSms(app);
sms.setDirection(IncomingMessage.Direction.Sent);
sms.setTo(cur.getString(addressIndex));
sms.setTimestamp(cur.getLong(dateIndex));
sms.setMessageBody(cur.getString(bodyIndex));
return sms;
}
public int getMessageCount()
{
return cur.getCount();
}
public void initListAdapter() {
// undocumented API; see
// core/java/android/provider/Telephony.java
Uri inboxUri = Uri.parse("content://sms/sent");
cur = getContentResolver().query(inboxUri,
new String[]{"_id", "address", "body", "date"}, null, null,
"_id desc limit 50");
final LayoutInflater inflater = (LayoutInflater)getSystemService(Context.LAYOUT_INFLATER_SERVICE);
final DateFormat dateFormat = new SimpleDateFormat("dd MMM HH:mm:ss");
CursorAdapter adapter = new CursorAdapter(this, cur) {
public View newView(Context context, Cursor cursor, ViewGroup parent) {
return inflater.inflate(R.layout.inbox_item, null);
}
public void bindView(View view, Context context, Cursor cursor)
{
TextView addrText = (TextView) view.findViewById(R.id.inbox_address);
TextView bodyText = (TextView) view.findViewById(R.id.inbox_body);
String address = cursor.getString(1);
String body = cursor.getString(2);
long date = cursor.getLong(3);
addrText.setText(address + " (" + dateFormat.format(new Date(date)) + ")");
bodyText.setText(body);
}
};
setListAdapter(adapter);
}
}

View File

@ -0,0 +1,78 @@
package org.envaya.sms.ui;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CursorAdapter;
import android.widget.TextView;
import org.envaya.sms.IncomingMessage;
import org.envaya.sms.IncomingSms;
import org.envaya.sms.R;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
public class MessagingSmsInbox extends MessagingForwarder {
private Cursor cur;
public IncomingMessage getMessageAtPosition(int position)
{
int addressIndex = cur.getColumnIndex("address");
int bodyIndex = cur.getColumnIndex("body");
int dateIndex = cur.getColumnIndex("date");
cur.moveToPosition(position);
IncomingSms sms = new IncomingSms(app);
sms.setFrom(cur.getString(addressIndex));
sms.setTimestamp(cur.getLong(dateIndex));
sms.setMessageBody(cur.getString(bodyIndex));
return sms;
}
public int getMessageCount()
{
return cur.getCount();
}
public void initListAdapter() {
// undocumented API; see
// core/java/android/provider/Telephony.java
Uri inboxUri = Uri.parse("content://sms/inbox");
cur = getContentResolver().query(inboxUri,
new String[]{"_id", "address", "body", "date"}, null, null,
"_id desc limit 50");
final LayoutInflater inflater = (LayoutInflater)getSystemService(Context.LAYOUT_INFLATER_SERVICE);
final DateFormat dateFormat = new SimpleDateFormat("dd MMM HH:mm:ss");
CursorAdapter adapter = new CursorAdapter(this, cur) {
public View newView(Context context, Cursor cursor, ViewGroup parent) {
return inflater.inflate(R.layout.inbox_item, null);
}
public void bindView(View view, Context context, Cursor cursor)
{
TextView addrText = (TextView) view.findViewById(R.id.inbox_address);
TextView bodyText = (TextView) view.findViewById(R.id.inbox_body);
String address = cursor.getString(1);
String body = cursor.getString(2);
long date = cursor.getLong(3);
addrText.setText(address + " (" + dateFormat.format(new Date(date)) + ")");
bodyText.setText(body);
}
};
setListAdapter(adapter);
}
}

View File

@ -130,7 +130,7 @@ public class PendingMessages extends ListActivity {
displayedMessages = messages;
this.setTitle("EnvayaSMS : Pending Messages ("+messages.size()+")");
this.setTitle(getText(R.string.pending_messages_title) + " ("+messages.size()+")");
final LayoutInflater inflater = (LayoutInflater)getSystemService(Context.LAYOUT_INFLATER_SERVICE);
final DateFormat longFormat = new SimpleDateFormat("dd MMM hh:mm:ss");

View File

@ -1,15 +1,16 @@
package org.envaya.sms.ui;
import android.content.SharedPreferences;
import android.content.BroadcastReceiver;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.Context;
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
import android.os.Bundle;
import android.preference.EditTextPreference;
import android.preference.ListPreference;
import android.preference.Preference;
import android.preference.PreferenceActivity;
import android.preference.PreferenceScreen;
import android.preference.*;
import android.provider.Settings;
import android.provider.Settings.SettingNotFoundException;
import android.text.method.PasswordTransformationMethod;
import android.view.Menu;
import org.envaya.sms.App;
import org.envaya.sms.R;
@ -18,6 +19,14 @@ public class Prefs extends PreferenceActivity implements OnSharedPreferenceChang
private App app;
private BroadcastReceiver installReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
PreferenceScreen screen = getPreferenceScreen();
updatePrefSummary(screen.findPreference("send_limit"));
}
};
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@ -32,6 +41,17 @@ public class Prefs extends PreferenceActivity implements OnSharedPreferenceChang
{
updatePrefSummary(screen.getPreference(i));
}
IntentFilter installReceiverFilter = new IntentFilter();
installReceiverFilter.addAction(App.EXPANSION_PACKS_CHANGED_INTENT);
registerReceiver(installReceiver, installReceiverFilter);
}
@Override
public void onDestroy()
{
unregisterReceiver(installReceiver);
super.onDestroy();
}
@Override
@ -50,11 +70,21 @@ public class Prefs extends PreferenceActivity implements OnSharedPreferenceChang
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
if (key.equals("outgoing_interval"))
{
app.setOutgoingMessageAlarm();
}
else if (key.startsWith("amqp_"))
{
if (app.isAmqpEnabled())
{
app.getAmqpConsumer().startAsync();
}
else
{
app.getAmqpConsumer().stopAsync();
}
}
else if (key.equals("wifi_sleep_policy"))
{
int value;
@ -107,15 +137,21 @@ public class Prefs extends PreferenceActivity implements OnSharedPreferenceChang
}
else if (key.equals("enabled"))
{
app.log(app.isEnabled() ? "SMS Gateway started." : "SMS Gateway stopped.");
app.log(app.isEnabled() ? getText(R.string.started) : getText(R.string.stopped));
app.enabledChanged();
}
sendBroadcast(new Intent(App.SETTINGS_CHANGED_INTENT));
updatePrefSummary(findPreference(key));
}
private void updatePrefSummary(Preference p)
{
if (p == null)
{
return;
}
String key = p.getKey();
if ("wifi_sleep_policy".equals(key))
@ -145,10 +181,31 @@ public class Prefs extends PreferenceActivity implements OnSharedPreferenceChang
break;
}
}
else if ("send_limit".equals(key))
{
int limit = app.getOutgoingMessageLimit();
String limitStr = "Send up to " + limit + " SMS per hour.";
if (limit < 300)
{
limitStr += "\nClick to increase limit...";
}
p.setSummary(limitStr);
}
else if ("help".equals(key))
{
p.setSummary(app.getPackageInfo().versionName);
}
else if (p instanceof PreferenceCategory)
{
PreferenceCategory category = (PreferenceCategory)p;
int numPreferences = category.getPreferenceCount();
for (int i = 0; i < numPreferences; i++)
{
updatePrefSummary(category.getPreference(i));
}
}
else if (p instanceof ListPreference) {
p.setSummary(((ListPreference)p).getEntry());
}
@ -160,7 +217,7 @@ public class Prefs extends PreferenceActivity implements OnSharedPreferenceChang
{
p.setSummary("(not set)");
}
else if (p.getKey().equals("password"))
else if (textPref.getEditText().getTransformationMethod() instanceof PasswordTransformationMethod)
{
p.setSummary("********");
}

View File

@ -8,6 +8,7 @@ import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.AdapterView;
import android.widget.CheckBox;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.ArrayAdapter;
import android.widget.EditText;
@ -20,6 +21,8 @@ public class TestPhoneNumbers extends ListActivity {
private App app;
private CheckBox autoAddOutgoing;
@Override
public void onCreate(Bundle icicle) {
super.onCreate(icicle);
@ -27,6 +30,9 @@ public class TestPhoneNumbers extends ListActivity {
app = (App)getApplication();
autoAddOutgoing = (CheckBox)findViewById(R.id.auto_add_outgoing);
autoAddOutgoing.setChecked(app.autoAddTestNumber());
ListView lv = getListView();
lv.setOnItemClickListener(new OnItemClickListener() {
public void onItemClick(AdapterView<?> parent, View view,
@ -63,6 +69,13 @@ public class TestPhoneNumbers extends ListActivity {
updateTestPhoneNumbers();
}
public void autoAddOutgoingClicked(View v)
{
boolean checked = autoAddOutgoing.isChecked();
app.log("Test Mode: automatically add outgoing message recipients set to " + (checked ? "YES" : "NO"));
app.saveBooleanSetting("auto_add_test_number", checked);
}
public void updateTestPhoneNumbers()
{
String[] senders = app.getTestPhoneNumbers().toArray(new String[]{});