5
0
mirror of https://github.com/cwinfo/envayasms.git synced 2024-12-04 12:35:32 +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

5
.gitignore vendored
View File

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

3
.gitmodules vendored
View File

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

@ -43,23 +43,22 @@
<fail <fail
message="sdk.dir is missing. Make sure to generate local.properties using 'android update project' or to inject it through an env var" message="sdk.dir is missing. Make sure to generate local.properties using 'android update project' or to inject it through an env var"
unless="sdk.dir" unless="sdk.dir"
/> />
<!--
<!-- extension targets. Uncomment the ones where you want to do custom work Import per project custom build rules if present at the root of the project.
in between standard targets --> This is the place to put custom intermediary targets such as:
<!-- -pre-build
<target name="-pre-build"> -pre-compile
</target> -post-compile (This is typically used for code obfuscation.
<target name="-pre-compile"> Compiled code location: ${out.classes.absolute.dir}
</target> If this is not done in place, override ${out.dex.input.absolute.dir})
-post-package
/* This is typically used for code obfuscation. -post-build
Compiled code location: ${out.classes.absolute.dir} -pre-clean
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. <!-- 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:orientation="vertical"
android:layout_width="fill_parent" android:layout_width="fill_parent"
android:layout_height="fill_parent" android:background="#333333"> android:layout_height="fill_parent" android:background="#333333">
<ScrollView android:layout_width="fill_parent"
android:layout_height="fill_parent" android:layout_weight="1">
<TextView <TextView
android:linksClickable="true" android:linksClickable="true"
android:drawablePadding="5px" android:drawablePadding="5px"
android:layout_width="fill_parent" android:layout_width="fill_parent"
android:layout_height="fill_parent" android:layout_height="wrap_content"
android:id="@+id/help" android:id="@+id/help"
android:autoLink="web" android:autoLink="web"
android:textColor="#FFFFFF" android:textColor="#FFFFFF"
android:layout_margin="5px"> android:layout_margin="5px">
</TextView> </TextView>
</ScrollView> <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> </LinearLayout>

View File

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

View File

@ -3,14 +3,46 @@
android:orientation="vertical" android:orientation="vertical"
android:layout_width="fill_parent" android:layout_width="fill_parent"
android:layout_height="fill_parent" android:background="#333333"> 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"> android:layout_height="fill_parent" android:layout_weight="1">
<TextView <TextView
android:layout_width="fill_parent" android:layout_width="fill_parent"
android:layout_height="fill_parent" android:layout_height="fill_parent"
android:id="@+id/info" android:id="@+id/log"
android:textColor="#FFFFFF" android:textColor="#FFFFFF"
android:textColorLink="#FFFFFF"
android:layout_margin="5px"></TextView> android:layout_margin="5px"></TextView>
</ScrollView> </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> </LinearLayout>

View File

@ -11,6 +11,13 @@
android:text="@string/test_phone_numbers"> android:text="@string/test_phone_numbers">
</TextView> </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" <ListView android:id="@android:id/list"
android:layout_width="fill_parent" android:layout_width="fill_parent"
android:layout_height="fill_parent" android:layout_height="fill_parent"

View File

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

View File

@ -1,18 +1,30 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<string name="app_name">EnvayaSMS</string> <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="settings">Settings</string>
<string name="test">Test Connection</string> <string name="test">Test Connection</string>
<string name="check_now">Check Messages</string> <string name="check_now">Check Messages</string>
<string name="help">Help</string> <string name="help">Help</string>
<string name="pending">Pending Msgs...</string> <string name="pending">Pending Msgs...</string>
<string name="retry_now">Retry</string> <string name="retry_now">Retry</string>
<string name="forward_inbox">Fwd Inbox...</string> <string name="forward_saved">Fwd Saved...</string>
<string name='service_started'>New SMS will be forwarded to server</string> <string name='service_started'>New messages will be forwarded to server</string>
<string name='test_phone_numbers'>When running EnvayaSMS in Test Mode, <string name='test_phone_numbers'>When running Telerivet in Test Mode,
EnvayaSMS will only forward SMS messages from the phone numbers EnvayaSMS will only forward SMS messages from the phone numbers
listed below. (Incoming SMS messages from other phone numbers will be saved 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'> <string name='ignored_phone_numbers'>
EnvayaSMS will ignore SMS messages from the phone numbers listed below. 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> 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,9 +6,11 @@
android:key="enabled" android:key="enabled"
android:title="Enable EnvayaSMS" android:title="Enable EnvayaSMS"
android:defaultValue='false' android:defaultValue='false'
android:summaryOn="All new SMS will be forwarded between phone and server" android:summaryOn="New messages will be forwarded between phone and server"
android:summaryOff="New SMS will not be forwarded between phone and server" android:summaryOff="New messages will not be forwarded between phone and server"
></CheckBoxPreference> ></CheckBoxPreference>
<PreferenceCategory android:title="Server Settings">
<EditTextPreference <EditTextPreference
android:key="server_url" android:key="server_url"
@ -37,36 +39,42 @@
android:entries="@array/check_intervals" android:entries="@array/check_intervals"
android:entryValues="@array/check_intervals_values" android:entryValues="@array/check_intervals_values"
></ListPreference> ></ListPreference>
</PreferenceCategory>
<PreferenceCategory android:title="Messaging Settings">
<CheckBoxPreference <CheckBoxPreference
android:key="keep_in_inbox" android:key="keep_in_inbox"
android:title="Keep new messages" android:title="Keep new messages"
android:summaryOff="Incoming SMS will not be stored in Messaging inbox" android:summaryOff="Incoming messages will not be stored in Messaging inbox"
android:summaryOn="Incoming SMS will be stored in Messaging inbox" android:summaryOn="Incoming messages will be stored in Messaging inbox"
></CheckBoxPreference> ></CheckBoxPreference>
<CheckBoxPreference <CheckBoxPreference
android:key="call_notifications" android:key="call_notifications"
android:title="Call notifications" android:title="Call notifications"
android:summaryOff="EnvayaSMS will not notify server when phone receives an incoming call" android:summaryOff="Telerivet will not notify server when phone receives an incoming call"
android:summaryOn="EnvayaSMS will notify server when phone receives an incoming call" android:summaryOn="Telerivet will notify server when phone receives an incoming call"
></CheckBoxPreference> ></CheckBoxPreference>
<ListPreference <PreferenceScreen
android:key="wifi_sleep_policy" android:key="send_limit"
android:title="Wi-Fi sleep policy" android:title="SMS rate limit"
android:defaultValue="never" android:summary="..."
android:entries="@array/wifi_sleep_policies" >
android:entryValues="@array/wifi_sleep_policies_values" <intent
> android:action="android.intent.action.MAIN"
</ListPreference> android:targetPackage="org.envaya.sms"
android:targetClass="org.envaya.sms.ui.ExpansionPacks" />
</PreferenceScreen>
<CheckBoxPreference <CheckBoxPreference
android:key="network_failover" android:key="forward_sent"
android:title="Network failover" android:title="Forward sent messages"
android:summaryOff="Do nothing if phone can't connect to server via Wi-Fi" android:summaryOff="SMS sent from Messaging app will not be forwarded to server"
android:summaryOn="Automatically switch to mobile data if phone can't connect to server via Wi-Fi" android:summaryOn="SMS sent from Messaging app will be forwarded to server"
></CheckBoxPreference> ></CheckBoxPreference>
<PreferenceScreen <PreferenceScreen
android:key="ignored_numbers" android:key="ignored_numbers"
@ -77,7 +85,7 @@
android:action="android.intent.action.MAIN" android:action="android.intent.action.MAIN"
android:targetPackage="org.envaya.sms" android:targetPackage="org.envaya.sms"
android:targetClass="org.envaya.sms.ui.IgnoredPhoneNumbers" /> android:targetClass="org.envaya.sms.ui.IgnoredPhoneNumbers" />
</PreferenceScreen> </PreferenceScreen>
<CheckBoxPreference <CheckBoxPreference
android:key="test_mode" android:key="test_mode"
@ -96,16 +104,114 @@
android:action="android.intent.action.MAIN" android:action="android.intent.action.MAIN"
android:targetPackage="org.envaya.sms" android:targetPackage="org.envaya.sms"
android:targetClass="org.envaya.sms.ui.TestPhoneNumbers" /> android:targetClass="org.envaya.sms.ui.TestPhoneNumbers" />
</PreferenceScreen> </PreferenceScreen>
</PreferenceCategory>
<PreferenceScreen <PreferenceCategory android:title="Networking Settings">
android:key="help"
android:title="About EnvayaSMS" <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 <intent
android:action="android.intent.action.MAIN" android:action="android.intent.action.MAIN"
android:targetPackage="org.envaya.sms" android:targetPackage="org.envaya.sms"
android:targetClass="org.envaya.sms.ui.Help" /> android:targetClass="org.envaya.sms.ui.Help" />
</PreferenceScreen> </PreferenceScreen>
</PreferenceCategory>
</PreferenceScreen> </PreferenceScreen>

View File

@ -1,24 +1,33 @@
<?php <?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_INCOMING = 'incoming';
const ACTION_OUTGOING = 'outgoing'; const ACTION_FORWARD_SENT = 'forward_sent';
const ACTION_SEND_STATUS = 'send_status'; const ACTION_SEND_STATUS = 'send_status';
const ACTION_DEVICE_STATUS = 'device_status'; const ACTION_DEVICE_STATUS = 'device_status';
const ACTION_TEST = 'test'; 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_QUEUED = 'queued';
const STATUS_FAILED = 'failed'; const STATUS_FAILED = 'failed';
const STATUS_SENT = 'sent'; const STATUS_SENT = 'sent';
const STATUS_CANCELLED = 'cancelled'; 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_CONNECTED = "power_connected";
const DEVICE_STATUS_POWER_DISCONNECTED = "power_disconnected"; const DEVICE_STATUS_POWER_DISCONNECTED = "power_disconnected";
const DEVICE_STATUS_BATTERY_LOW = "battery_low"; const DEVICE_STATUS_BATTERY_LOW = "battery_low";
@ -29,8 +38,10 @@ class EnvayaSMS
const MESSAGE_TYPE_MMS = 'mms'; const MESSAGE_TYPE_MMS = 'mms';
const MESSAGE_TYPE_CALL = 'call'; const MESSAGE_TYPE_CALL = 'call';
const NETWORK_MOBILE = "MOBILE"; // power source constants same as from Android's BatteryManager.EXTRA_PLUGGED
const NETWORK_WIFI = "WIFI"; const POWER_SOURCE_BATTERY = 0;
const POWER_SOURCE_AC = 1;
const POWER_SOURCE_USB = 2;
static function escape($val) static function escape($val)
{ {
@ -41,60 +52,36 @@ class EnvayaSMS
static function get_request() static function get_request()
{ {
if (!isset(static::$request)) if (!isset(self::$request))
{ {
$version = @$_POST['version']; $version = @$_POST['version'];
// If API version changes, could return if (isset($_POST['action']))
// different EnvayaSMS_Request instance {
// to support multiple phone versions self::$request = new EnvayaSMS_ActionRequest();
}
static::$request = new EnvayaSMS_Request(); else
{
self::$request = new EnvayaSMS_Request();
}
} }
return static::$request; return self::$request;
} }
static function get_error_xml($message)
{
ob_start();
echo "<?xml version='1.0' encoding='UTF-8'?>\n";
echo "<response>";
echo "<error>";
echo EnvayaSMS::escape($message);
echo "</error>";
echo "</response>";
return ob_get_clean();
}
static function get_success_xml()
{
ob_start();
echo "<?xml version='1.0' encoding='UTF-8'?>\n";
echo "<response></response>";
return ob_get_clean();
}
} }
class EnvayaSMS_Request class EnvayaSMS_Request
{ {
private $request_action;
public $version; public $version;
public $phone_number;
public $log;
public $version_name; public $version_name;
public $sdk_int; public $sdk_int;
public $manufacturer; public $manufacturer;
public $model; public $model;
public $network;
function __construct() function __construct()
{ {
$this->version = $_POST['version']; $this->version = (int)@$_POST['version'];
$this->phone_number = $_POST['phone_number'];
$this->log = $_POST['log'];
$this->network = @$_POST['network'];
if (preg_match('#/(?P<version_name>[\w\.\-]+) \(Android; SDK (?P<sdk_int>\d+); (?P<manufacturer>[^;]*); (?P<model>[^\)]*)\)#', if (preg_match('#/(?P<version_name>[\w\.\-]+) \(Android; SDK (?P<sdk_int>\d+); (?P<manufacturer>[^;]*); (?P<model>[^\)]*)\)#',
@$_SERVER['HTTP_USER_AGENT'], $matches)) @$_SERVER['HTTP_USER_AGENT'], $matches))
@ -103,9 +90,115 @@ class EnvayaSMS_Request
$this->sdk_int = $matches['sdk_int']; $this->sdk_int = $matches['sdk_int'];
$this->manufacturer = $matches['manufacturer']; $this->manufacturer = $matches['manufacturer'];
$this->model = $matches['model']; $this->model = $matches['model'];
}
}
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() function get_action()
{ {
if (!$this->request_action) if (!$this->request_action)
@ -121,7 +214,9 @@ class EnvayaSMS_Request
{ {
case EnvayaSMS::ACTION_INCOMING: case EnvayaSMS::ACTION_INCOMING:
return new EnvayaSMS_Action_Incoming($this); return new EnvayaSMS_Action_Incoming($this);
case EnvayaSMS::ACTION_OUTGOING: case EnvayaSMS::ACTION_FORWARD_SENT:
return new EnvayaSMS_Action_ForwardSent($this);
case EnvayaSMS::ACTION_OUTGOING:
return new EnvayaSMS_Action_Outgoing($this); return new EnvayaSMS_Action_Outgoing($this);
case EnvayaSMS::ACTION_SEND_STATUS: case EnvayaSMS::ACTION_SEND_STATUS:
return new EnvayaSMS_Action_SendStatus($this); return new EnvayaSMS_Action_SendStatus($this);
@ -129,6 +224,8 @@ class EnvayaSMS_Request
return new EnvayaSMS_Action_Test($this); return new EnvayaSMS_Action_Test($this);
case EnvayaSMS::ACTION_DEVICE_STATUS: case EnvayaSMS::ACTION_DEVICE_STATUS:
return new EnvayaSMS_Action_DeviceStatus($this); return new EnvayaSMS_Action_DeviceStatus($this);
case EnvayaSMS::ACTION_AMQP_STARTED:
return new EnvayaSMS_Action_AmqpStarted($this);
default: default:
return new EnvayaSMS_Action($this); return new EnvayaSMS_Action($this);
} }
@ -163,29 +260,8 @@ class EnvayaSMS_Request
$input .= ",$password"; $input .= ",$password";
//error_log("Signed data: '$input'");
return base64_encode(sha1($input, true)); 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 class EnvayaSMS_OutgoingMessage
@ -197,6 +273,10 @@ class EnvayaSMS_OutgoingMessage
public $type; // EnvayaSMS::MESSAGE_TYPE_* value (default sms) 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 class EnvayaSMS_Action
{ {
public $type; 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; // 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 $message_type; // EnvayaSMS::MESSAGE_TYPE_MMS or EnvayaSMS::MESSAGE_TYPE_SMS
public $mms_parts; // array of EnvayaSMS_MMS_Part instances public $mms_parts; // array of EnvayaSMS_MMS_Part instances
public $timestamp; // timestamp of incoming message (added in version 12) public $timestamp; // timestamp of incoming message (added in version 12)
public $age; // delay in ms between time when message originally received and when forwarded to server (added in version 18)
function __construct($request) function __construct($request)
{ {
parent::__construct($request); parent::__construct($request);
$this->type = EnvayaSMS::ACTION_INCOMING;
$this->from = $_POST['from'];
$this->message = @$_POST['message']; $this->message = @$_POST['message'];
$this->message_type = $_POST['message_type']; $this->message_type = $_POST['message_type'];
$this->timestamp = @$_POST['timestamp']; $this->timestamp = @$_POST['timestamp'];
$this->age = @$_POST['age'];
if ($this->message_type == EnvayaSMS::MESSAGE_TYPE_MMS) if ($this->message_type == EnvayaSMS::MESSAGE_TYPE_MMS)
{ {
@ -260,26 +335,52 @@ class EnvayaSMS_Action_Incoming extends EnvayaSMS_Action
$this->mms_parts[] = new EnvayaSMS_MMS_Part($mms_part); $this->mms_parts[] = new EnvayaSMS_MMS_Part($mms_part);
} }
} }
} }
}
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'];
}
}
class EnvayaSMS_Action_Outgoing extends EnvayaSMS_Action class EnvayaSMS_Action_Outgoing extends EnvayaSMS_Action
{ {
function __construct($request) function __construct($request)
{ {
parent::__construct($request); parent::__construct($request);
$this->type = EnvayaSMS::ACTION_OUTGOING; $this->type = EnvayaSMS::ACTION_OUTGOING;
} }
function get_response_xml($messages)
{
return $this->request->get_messages_xml($messages);
}
} }
class EnvayaSMS_Action_Test extends EnvayaSMS_Action class EnvayaSMS_Action_Test extends EnvayaSMS_Action
@ -318,3 +419,89 @@ class EnvayaSMS_Action_DeviceStatus extends EnvayaSMS_Action
$this->status = $_POST['status']; $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,19 +10,21 @@ PHP web server, by running the following commands:
git submodule init git submodule init
php server.php 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: On a phone running EnvayaSMS, go to Menu -> Settings and enter:
* Server URL: The URL to example/www/index.php. * Server URL: The URL to example/www/gateway.php.
If you're using server.php, this will be http://<your_ip_address>:8002/ If you're using server.php, this will be http://<your_ip_address>:8002/gateway.php
* Your phone number: One of the phone numbers listed in example/config.php * Your phone number: The phone number of your Android phone
* Password: The corresponding password in example/config.php * Password: The password in example/config.php
To send an outgoing SMS, use To send an outgoing SMS, use
php example/send_sms.php php example/send_sms.php
example/www/test.html allows you to simulate the HTTP requests made by EnvayaSMS example/www/test.html allows you to simulate the HTTP requests made by EnvayaSMS
in your browser without actually using the EnvayaSMS app. 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 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 <?php
$PASSWORDS = array( ini_set('display_errors','0');
'16505551212' => 'rosebud',
'16505551213' => 's3krit',
);
$PHONE_NUMBERS = array_keys($PASSWORDS);
$OUTGOING_DIR_NAME = __DIR__."/outgoing_sms"; /*
* 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 <?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 * This example script queues outgoing messages using the local filesystem.
* EnvayaSMS checks for outgoing messages. * The messages are sent the next time EnvayaSMS sends an ACTION_OUTGOING request to www/gateway.php.
*/ */
require_once __DIR__."/config.php"; require_once __DIR__."/config.php";
require_once dirname(__DIR__)."/EnvayaSMS.php";
$arg_len = sizeof($argv); if (sizeof($argv) == 3)
if ($arg_len == 4)
{
$from = $argv[1];
$to = $argv[2];
$message = $argv[3];
}
else if ($arg_len == 3)
{ {
$from = $PHONE_NUMBERS[0];
$to = $argv[1]; $to = $argv[1];
$message = $argv[2]; $body = $argv[2];
} }
else else
{ {
error_log("Usage: php send_sms.php [<from>] <to> \"<message>\""); error_log("Usage: php send_sms.php <to> \"<message>\"");
error_log("Examples: "); error_log("Example: ");
error_log(" php send_sms.php 16505551212 16504449876 \"hello world\"");
error_log(" php send_sms.php 16504449876 \"hello world\""); error_log(" php send_sms.php 16504449876 \"hello world\"");
die; 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( echo "Message {$message->id} added to filesystem queue\n";
'from' => $from,
'to' => $to,
'message' => $message,
'id' => $id
)));
echo "Message $id added to outgoing 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 <?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(__DIR__)."/config.php";
require_once dirname(dirname(__DIR__))."/EnvayaSMS.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(); $request = EnvayaSMS::get_request();
$phone_number = $request->phone_number; header("Content-Type: {$request->get_response_type()}");
$password = @$PASSWORDS[$phone_number]; if (!$request->is_validated($PASSWORD))
header("Content-Type: text/xml");
if (!isset($password) || !$request->is_validated($password))
{ {
header("HTTP/1.1 403 Forbidden"); header("HTTP/1.1 403 Forbidden");
error_log("Invalid request signature"); error_log("Invalid password");
echo EnvayaSMS::get_error_xml("Invalid request signature"); echo $request->render_error_response("Invalid password");
return; 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(); $action = $request->get_action();
switch ($action->type) switch ($action->type)
{ {
case EnvayaSMS::ACTION_INCOMING: case EnvayaSMS::ACTION_INCOMING:
// Send an auto-reply for each incoming message.
$type = strtoupper($action->message_type); $type = strtoupper($action->message_type);
error_log("Received $type from {$action->from}"); error_log("Received $type from {$action->from}");
@ -62,25 +56,29 @@ switch ($action->type)
$reply->message = "You said: {$action->message}"; $reply->message = "You said: {$action->message}";
error_log("Sending reply: {$reply->message}"); 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; return;
case EnvayaSMS::ACTION_OUTGOING: case EnvayaSMS::ACTION_OUTGOING:
$messages = array(); $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); $dir = opendir($OUTGOING_DIR_NAME);
while ($file = readdir($dir)) while ($file = readdir($dir))
{ {
if (preg_match('#\.json$#', $file)) if (preg_match('#\.json$#', $file))
{ {
$data = json_decode(file_get_contents("$OUTGOING_DIR_NAME/$file"), true); $data = json_decode(file_get_contents("$OUTGOING_DIR_NAME/$file"), true);
if ($data && @$data['from'] == $phone_number) if ($data)
{ {
$sms = new EnvayaSMS_OutgoingMessage(); $sms = new EnvayaSMS_OutgoingMessage();
$sms->id = $data['id']; $sms->id = $data['id'];
$sms->to = $data['to']; $sms->to = $data['to'];
$sms->from = $data['from'];
$sms->message = $data['message']; $sms->message = $data['message'];
$messages[] = $sms; $messages[] = $sms;
} }
@ -88,33 +86,40 @@ switch ($action->type)
} }
closedir($dir); closedir($dir);
echo $action->get_response_xml($messages); $events = array();
if ($messages)
{
$events[] = new EnvayaSMS_Event_Send($messages);
}
echo $request->render_response($events);
return; return;
case EnvayaSMS::ACTION_SEND_STATUS: case EnvayaSMS::ACTION_SEND_STATUS:
$id = $action->id; $id = $action->id;
error_log("message $id status: {$action->status}");
// delete file with matching id // delete file with matching id
if (preg_match('#^\w+$#', $id) && unlink("$OUTGOING_DIR_NAME/$id.json")) if (preg_match('#^\w+$#', $id))
{ {
echo EnvayaSMS::get_success_xml(); unlink("$OUTGOING_DIR_NAME/$id.json");
} }
else echo $request->render_response();
{
header("HTTP/1.1 404 Not Found");
echo EnvayaSMS::get_error_xml("Invalid id");
}
return; return;
case EnvayaSMS::ACTION_DEVICE_STATUS: case EnvayaSMS::ACTION_DEVICE_STATUS:
error_log("device_status = {$action->status}"); error_log("device_status = {$action->status}");
echo EnvayaSMS::get_success_xml(); echo $request->render_response();
return; return;
case EnvayaSMS::ACTION_TEST: case EnvayaSMS::ACTION_TEST:
echo EnvayaSMS::get_success_xml(); echo $request->render_response();
return; return;
default: default:
header("HTTP/1.1 404 Not Found"); 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; 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>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>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>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()'> <tr><th>Action</th><td><select id='action' onchange='actionChanged()' onkeypress='actionChanged()'>
<option value='incoming'>incoming</option> <option value='incoming'>incoming</option>
<option value='outgoing'>outgoing</option> <option value='outgoing'>outgoing</option>
<option value='send_status'>send_status</option> <option value='send_status'>send_status</option>
<option value='device_status'>device_status</option> <option value='device_status'>device_status</option>
<option value='test'>test</option> <option value='test'>test</option>
<option value='amqp_started'>amqp_started</option>
</select></td></tr> </select></td></tr>
</table> </table>
<div id='action_incoming'> <div id='action_incoming'>
@ -47,6 +60,7 @@ body
<tr><th>Message Type</th><td><select id='message_type'> <tr><th>Message Type</th><td><select id='message_type'>
<option value='sms'>sms</option> <option value='sms'>sms</option>
<option value='mms'>mms</option> <option value='mms'>mms</option>
<option value='call'>call</option>
</select></td></tr> </select></td></tr>
<tr><th>Message</th><td><textarea id='message' style='width:250px'></textarea></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> <tr><th>Timestamp</th><td><input id='timestamp' type='text' /></td></tr>
@ -64,6 +78,7 @@ body
<option value='sent'>sent</option> <option value='sent'>sent</option>
<option value='failed'>failed</option> <option value='failed'>failed</option>
<option value='queued'>queued</option> <option value='queued'>queued</option>
<option value='cancelled'>cancelled</option>
</select></td></tr> </select></td></tr>
<tr><th>Error Message</th><td><input id='error' type='text' size='50' /></td></tr> <tr><th>Error Message</th><td><input id='error' type='text' size='50' /></td></tr>
</table> </table>
@ -80,10 +95,16 @@ body
<option value='power_disconnected'>power_disconnected</option> <option value='power_disconnected'>power_disconnected</option>
<option value='battery_low'>battery_low</option> <option value='battery_low'>battery_low</option>
<option value='battery_okay'>battery_okay</option> <option value='battery_okay'>battery_okay</option>
<option value='send_limit_exceeded'>send_limit_exceeded</option>
</select></td></tr> </select></td></tr>
</table> </table>
</div> </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'> <script type='text/javascript'>
@ -110,11 +131,17 @@ function performAction() {
var action = $('action').value; var action = $('action').value;
var params = { var params = {
version: '13', version: '29',
phone_number: $('phone_number').value, phone_number: $('phone_number').value,
action: action, 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') if (action == 'incoming')
{ {
@ -133,6 +160,10 @@ function performAction() {
{ {
params.status = $('device_status').value; 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(); var xhr = (window.ActiveXObject && !window.XMLHttpRequest) ? new ActiveXObject("Msxml2.XMLHTTP") : new XMLHttpRequest();
@ -189,8 +220,9 @@ function performAction() {
xhr.send(paramStr); 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(); $('timestamp').value = new Date().getTime();
$('now').value = new Date().getTime();
</script> </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; package org.envaya.sms;
import org.envaya.sms.service.EnabledChangedService;
import android.app.Activity; import android.app.Activity;
import android.app.AlarmManager; import android.app.AlarmManager;
import android.app.Application; import android.app.Application;
@ -18,8 +19,6 @@ import android.os.AsyncTask;
import android.os.Bundle; import android.os.Bundle;
import android.os.SystemClock; import android.os.SystemClock;
import android.preference.PreferenceManager; import android.preference.PreferenceManager;
import android.telephony.PhoneStateListener;
import android.telephony.TelephonyManager;
import android.text.Html; import android.text.Html;
import android.text.SpannableStringBuilder; import android.text.SpannableStringBuilder;
import android.util.Log; import android.util.Log;
@ -54,9 +53,17 @@ public final class App extends Application {
public static final String ACTION_OUTGOING = "outgoing"; public static final String ACTION_OUTGOING = "outgoing";
public static final String ACTION_INCOMING = "incoming"; 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_SEND_STATUS = "send_status";
public static final String ACTION_DEVICE_STATUS = "device_status"; public static final String ACTION_DEVICE_STATUS = "device_status";
public static final String ACTION_TEST = "test"; 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_QUEUED = "queued";
public static final String STATUS_FAILED = "failed"; public static final String STATUS_FAILED = "failed";
@ -77,7 +84,10 @@ public final class App extends Application {
// intent to signal to Main activity (if open) that log has changed // 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 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 // signal to PendingMessages activity (if open) that inbox/outbox has changed
public static final String INBOX_CHANGED_INTENT = "org.envaya.sms.INBOX_CHANGED"; public static final String INBOX_CHANGED_INTENT = "org.envaya.sms.INBOX_CHANGED";
public static final String OUTBOX_CHANGED_INTENT = "org.envaya.sms.OUTBOX_CHANGED"; public static final String OUTBOX_CHANGED_INTENT = "org.envaya.sms.OUTBOX_CHANGED";
@ -134,7 +144,8 @@ public final class App extends Application {
public final Queue<HttpTask> queuedTasks = new LinkedList<HttpTask>(); public final Queue<HttpTask> queuedTasks = new LinkedList<HttpTask>();
private SharedPreferences settings; private SharedPreferences settings;
private MmsObserver mmsObserver; private MessagingObserver messagingObserver;
private SpannableStringBuilder displayedLog = new SpannableStringBuilder(); private SpannableStringBuilder displayedLog = new SpannableStringBuilder();
private long lastLogTime; private long lastLogTime;
@ -151,24 +162,39 @@ public final class App extends Application {
// count to provide round-robin selection of expansion packs // count to provide round-robin selection of expansion packs
private int outgoingMessageCount = -1; private int outgoingMessageCount = -1;
private MmsUtils mmsUtils; private MessagingUtils messagingUtils;
private CallListener callListener; private CallListener callListener;
private DatabaseHelper dbHelper;
private AmqpConsumer amqpConsumer;
private boolean connectivityError = false; private boolean connectivityError = false;
@Override @Override
public void onCreate() public void onCreate()
{ {
super.onCreate(); 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); settings = PreferenceManager.getDefaultSharedPreferences(this);
mmsUtils = new MmsUtils(this); messagingUtils = new MessagingUtils(this);
callListener = new CallListener(this); callListener = new CallListener(this);
outgoingMessagePackages.add(getPackageName()); outgoingMessagePackages.add(getPackageName());
mmsObserver = new MmsObserver(this); messagingObserver = new MessagingObserver(this);
dbHelper = new DatabaseHelper(this);
amqpConsumer = new AmqpConsumer(this);
try try
{ {
@ -187,92 +213,30 @@ public final class App extends Application {
public void configuredChanged() public void configuredChanged()
{ {
log(Html.fromHtml( if (isConfigured())
isEnabled() ? "<b>SMS gateway running ("+getDisplayString(getPhoneNumber())+").</b>"
: "<b>SMS gateway disabled.</b>"));
log("Server URL: " + getDisplayString(getServerUrl()));
log("Keep new messages: " + (getKeepInInbox() ? "YES": "NO"));
log("Call notifications: " + (callNotificationsEnabled() ? "ON": "OFF"));
log("Network failover: " + (isNetworkFailoverEnabled() ? "ON": "OFF"));
boolean ignoreShortcodes = ignoreShortcodes();
boolean ignoreNonNumeric = ignoreNonNumeric();
List<String> ignoredNumbers = getIgnoredPhoneNumbers();
if (ignoredNumbers.size() > 0 || ignoreShortcodes || ignoreNonNumeric)
{ {
log("Ignored phone numbers:"); sendBroadcast(new Intent(App.SETTINGS_CHANGED_INTENT));
enabledChanged();
if (ignoreShortcodes || ignoreNonNumeric)
{
String ignoreDesc = " ";
if (ignoreShortcodes)
{
ignoreDesc += "all shortcodes";
}
if (ignoreShortcodes && ignoreNonNumeric)
{
ignoreDesc += ", ";
}
if (ignoreNonNumeric)
{
ignoreDesc += "all non-numeric";
}
log(ignoreDesc);
}
for (String sender : ignoredNumbers)
{
log(" " + sender);
}
} }
}
if (isTestMode())
{
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() public void enabledChanged()
{ {
TelephonyManager telephony = (TelephonyManager) // startup/shutdown tasks may be slow, so offload them to a worker thread...
getSystemService(Context.TELEPHONY_SERVICE); // IntentService takes care of only running one request at a time
startService(new Intent(this, EnabledChangedService.class));
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));
}
public PackageInfo getPackageInfo() public PackageInfo getPackageInfo()
{ {
return packageInfo; return packageInfo;
} }
public boolean isSmsExpansionPackInstalled(String packageName)
{
return outgoingMessagePackages.contains(packageName);
}
public synchronized String chooseOutgoingSmsPackage(int numParts) public synchronized String chooseOutgoingSmsPackage(int numParts)
{ {
outgoingMessageCount++; outgoingMessageCount++;
@ -388,8 +352,9 @@ public final class App extends Application {
if (prevLimit != newLimit) 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() public int getOutgoingMessageLimit()
@ -442,11 +407,11 @@ public final class App extends Application {
{ {
String serverUrl = getServerUrl(); String serverUrl = getServerUrl();
if (serverUrl.length() > 0) { if (serverUrl.length() > 0) {
log("Checking for outgoing messages"); log("Checking for messages");
pollActive = true; pollActive = true;
new PollerTask(this).execute(); new PollerTask(this).execute();
} else { } else {
log("Can't check outgoing messages; server URL not set"); log("Can't check messages; server URL not set");
} }
} }
else else
@ -490,22 +455,46 @@ public final class App extends Application {
return str; return str;
} }
} }
public boolean isConfigured()
{
return getServerUrl().length() > 0;
}
public boolean callNotificationsEnabled() public boolean callNotificationsEnabled()
{ {
return tryGetBooleanSetting("call_notifications", false); return tryGetBooleanSetting("call_notifications", false);
} }
public String getConfigureServer() {
return settings.getString("configure_server", "");
}
public String getServerUrl() { public String getServerUrl() {
return settings.getString("server_url", ""); return settings.getString("server_url", "");
} }
public String getPhoneNumber() { public String getPhoneNumber() {
return settings.getString("phone_number", ""); return settings.getString("phone_number", "");
} }
public int getOutgoingPollSeconds() { public boolean isAmqpEnabled()
return Integer.parseInt(settings.getString("outgoing_interval", "0")); {
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() public boolean isEnabled()
@ -513,13 +502,30 @@ public final class App extends Application {
return tryGetBooleanSetting("enabled", false); 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) public boolean tryGetBooleanSetting(String name, boolean defaultValue)
{ {
try try
{ {
return settings.getBoolean(name, defaultValue); return settings.getBoolean(name, defaultValue);
} }
catch (Exception ex) catch (ClassCastException ex)
{ {
return defaultValue; return defaultValue;
} }
@ -530,10 +536,20 @@ public final class App extends Application {
return tryGetBooleanSetting("network_failover", false); return tryGetBooleanSetting("network_failover", false);
} }
public boolean isForwardingSentMessagesEnabled()
{
return tryGetBooleanSetting("forward_sent", false);
}
public boolean isTestMode() public boolean isTestMode()
{ {
return tryGetBooleanSetting("test_mode", false); return tryGetBooleanSetting("test_mode", false);
} }
public boolean autoAddTestNumber()
{
return tryGetBooleanSetting("auto_add_test_number", false);
}
public boolean getKeepInInbox() public boolean getKeepInInbox()
{ {
@ -542,12 +558,17 @@ public final class App extends Application {
public boolean ignoreShortcodes() public boolean ignoreShortcodes()
{ {
return tryGetBooleanSetting("ignore_shortcodes", true); return tryGetBooleanSetting("ignore_shortcodes", false);
} }
public boolean ignoreNonNumeric() 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() { public String getPassword() {
@ -628,15 +649,25 @@ public final class App extends Application {
sendBroadcast(new Intent(App.LOG_CHANGED_INTENT)); 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. * Changes whenever we change the beginning of the displayed log.
* If it doesn't change, the Main activity can update the log view much * If it doesn't change, the Main activity can update the log view much
* faster by using TextView.append() instead of TextView.setText() * faster by using TextView.append() instead of TextView.setText()
*/ */
public int getLogEpoch() public synchronized int getLogEpoch()
{ {
return logEpoch; return logEpoch;
} }
public synchronized CharSequence getDisplayedLog() public synchronized CharSequence getDisplayedLog()
{ {
@ -665,9 +696,9 @@ public final class App extends Application {
} }
} }
public MmsUtils getMmsUtils() public MessagingUtils getMessagingUtils()
{ {
return mmsUtils; return messagingUtils;
} }
private List<String> testPhoneNumbers; private List<String> testPhoneNumbers;
@ -763,20 +794,48 @@ public final class App extends Application {
return values; 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) public boolean isForwardablePhoneNumber(String phoneNumber)
{ {
if (isTestMode()) if (isTestMode())
{ {
return getTestPhoneNumbers().contains(phoneNumber); return isPhoneNumberInList(phoneNumber, getTestPhoneNumbers());
} }
if (getIgnoredPhoneNumbers().contains(phoneNumber)) if (isPhoneNumberInList(phoneNumber, getIgnoredPhoneNumbers()))
{ {
return false; return false;
} }
int numDigits = 0; int numDigits = 0;
int length = phoneNumber.length(); int length = (phoneNumber == null) ? 0 : phoneNumber.length();
for (int i = 0; i < length; i++) for (int i = 0; i < length; i++)
{ {
@ -949,15 +1008,19 @@ public final class App extends Application {
if (networkInfo == null || !networkInfo.isConnected()) if (networkInfo == null || !networkInfo.isConnected())
{ {
amqpConsumer.stopAsync();
return; return;
} }
amqpConsumer.startDelayed(5000);
int networkType = networkInfo.getType(); int networkType = networkInfo.getType();
if (networkType == activeNetworkType) if (networkType == activeNetworkType)
{ {
return; return;
} }
activeNetworkType = networkType; activeNetworkType = networkType;
log("Connected to " + networkInfo.getTypeName()); log("Connected to " + networkInfo.getTypeName());
@ -1007,5 +1070,25 @@ public final class App extends Application {
public synchronized void addQueuedTask(HttpTask task) public synchronized void addQueuedTask(HttpTask task)
{ {
queuedTasks.add(task); 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); incomingMessages.put(uri, message);
app.log("Received "+message.getDescription()); app.log("Received "+message.getDisplayType());
app.getDatabaseHelper().insertPendingMessage(message);
enqueueMessage(message); enqueueMessage(message);
} }
@ -83,6 +85,8 @@ public class Inbox {
{ {
numForwardingMessages--; numForwardingMessages--;
} }
app.getDatabaseHelper().deletePendingMessage(message);
app.log(message.getDescription() + " deleted"); app.log(message.getDescription() + " deleted");
notifyChanged(); notifyChanged();
@ -98,7 +102,8 @@ public class Inbox {
} }
notifyChanged(); notifyChanged();
numForwardingMessages--; numForwardingMessages--;
maybeDequeueMessage(); maybeDequeueMessage();
} }
@ -111,17 +116,12 @@ public class Inbox {
notifyChanged(); notifyChanged();
if (message instanceof IncomingMms) message.onForwardComplete();
{
IncomingMms mms = (IncomingMms)message;
if (!app.getKeepInInbox())
{
app.log("Deleting MMS " + mms.getId() + " from inbox...");
app.getMmsUtils().deleteFromInbox(mms);
}
}
numForwardingMessages--; numForwardingMessages--;
app.getDatabaseHelper().deletePendingMessage(message);
maybeDequeueMessage(); maybeDequeueMessage();
} }

View File

@ -5,25 +5,17 @@
package org.envaya.sms; package org.envaya.sms;
import android.net.Uri; import android.net.Uri;
import org.envaya.sms.task.ForwarderTask;
public class IncomingCall extends IncomingMessage { public class IncomingCall extends IncomingMessage {
private long id;
private static long nextId = 1;
public IncomingCall(App app, String from, long timestampMillis) public IncomingCall(App app, String from, long timestampMillis)
{ {
super(app, from, timestampMillis); super(app, from, timestampMillis);
this.id = getNextId();
} }
public static synchronized long getNextId() public IncomingCall(App app)
{ {
long id = nextId; super(app);
nextId++;
return id;
} }
public String getDisplayType() public String getDisplayType()
@ -44,6 +36,6 @@ public class IncomingCall extends IncomingMessage {
public Uri getUri() 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 { 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 String message = "";
protected long timestamp; // unix timestamp in milliseconds protected long timestamp; // unix timestamp in milliseconds
protected long timeReceived; // SystemClock.elapsedRealtime protected long timeCreated; // SystemClock.elapsedRealtime
private ProcessingState state = ProcessingState.None; 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 public enum ProcessingState
{ {
None, // not doing anything with this sms now... just sitting around None, // not doing anything with this sms now... just sitting around
@ -25,13 +40,33 @@ public abstract class IncomingMessage extends QueuedMessage {
Forwarded Forwarded
} }
public IncomingMessage(App app)
{
super(app);
}
public IncomingMessage(App app, String from, long timestamp) public IncomingMessage(App app, String from, long timestamp)
{ {
super(app); super(app);
this.from = from;
this.timestamp = timestamp;
this.timeReceived = SystemClock.elapsedRealtime(); if (from == null)
{
from = "";
}
this.from = from;
this.timestamp = timestamp;
this.timeCreated = SystemClock.elapsedRealtime();
}
public void setDirection(Direction direction)
{
this.direction = direction;
}
public Direction getDirection()
{
return this.direction;
} }
public String getMessageBody() public String getMessageBody()
@ -39,9 +74,14 @@ public abstract class IncomingMessage extends QueuedMessage {
return message; return message;
} }
public void setMessageBody(String message)
{
this.message = message;
}
public long getAge() public long getAge()
{ {
return SystemClock.elapsedRealtime() - timeReceived; return SystemClock.elapsedRealtime() - timeCreated;
} }
public long getTimestamp() public long getTimestamp()
@ -49,6 +89,11 @@ public abstract class IncomingMessage extends QueuedMessage {
return timestamp; return timestamp;
} }
public void setTimestamp(long timestamp)
{
this.timestamp = timestamp;
}
public ProcessingState getProcessingState() public ProcessingState getProcessingState()
{ {
return state; return state;
@ -61,14 +106,47 @@ public abstract class IncomingMessage extends QueuedMessage {
public boolean isForwardable() 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() public String getFrom()
{ {
return from; 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() { protected Intent getRetryIntent() {
Intent intent = new Intent(app, IncomingMessageRetry.class); Intent intent = new Intent(app, IncomingMessageRetry.class);
intent.setData(this.getUri()); intent.setData(this.getUri());
@ -92,7 +170,14 @@ public abstract class IncomingMessage extends QueuedMessage {
public String getDescription() public String getDescription()
{ {
return getDisplayType() + " from " + getFrom(); if (direction == Direction.Sent)
{
return "Sent " + getDisplayType() + " to " + getTo();
}
else
{
return getDisplayType() + " from " + getFrom();
}
} }
public void tryForwardToServer() public void tryForwardToServer()
@ -107,15 +192,30 @@ public abstract class IncomingMessage extends QueuedMessage {
public abstract String getMessageType(); public abstract String getMessageType();
public void onForwardComplete()
{
}
protected ForwarderTask getForwarderTask() protected ForwarderTask getForwarderTask()
{ {
return new ForwarderTask(this, ForwarderTask task = new ForwarderTask(this,
new BasicNameValuePair("message_type", getMessageType()), new BasicNameValuePair("message_type", getMessageType()),
new BasicNameValuePair("message", getMessageBody()), new BasicNameValuePair("message", getMessageBody()),
new BasicNameValuePair("action", App.ACTION_INCOMING), new BasicNameValuePair("timestamp", "" + getTimestamp())
new BasicNameValuePair("from", getFrom()),
new BasicNameValuePair("timestamp", "" + getTimestamp()),
new BasicNameValuePair("age", "" + getAge())
); );
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,59 +14,60 @@ import org.apache.http.entity.mime.content.ContentBody;
import org.envaya.sms.task.ForwarderTask; import org.envaya.sms.task.ForwarderTask;
public class IncomingMms extends IncomingMessage { public class IncomingMms extends IncomingMessage {
List<MmsPart> parts; private List<MmsPart> parts;
long id;
String contentLocation;
public IncomingMms(App app, String from, long timestamp, long id) public IncomingMms(App app, long timestamp, long messagingId)
{ {
super(app, from, timestamp); super(app, null, timestamp);
this.parts = new ArrayList<MmsPart>(); this.messagingId = messagingId;
this.id = id; }
}
public IncomingMms(App app)
{
super(app);
}
public String getDisplayType() @Override
public String getFrom()
{ {
return "MMS"; 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() 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; return parts;
}
public void addPart(MmsPart part)
{
parts.add(part);
} }
public long getId() public String getDisplayType()
{ {
return id; return "MMS";
} }
public String getContentLocation()
{
return contentLocation;
}
public void setContentLocation(String contentLocation)
{
this.contentLocation = contentLocation;
}
@Override @Override
public String toString() public String toString()
{ {
StringBuilder builder = new StringBuilder(); StringBuilder builder = new StringBuilder();
builder.append("MMS id="); builder.append("MMS id=");
builder.append(id); builder.append(messagingId);
builder.append(" from="); builder.append(" from=");
builder.append(from); builder.append(from);
builder.append(":\n"); builder.append(":\n");
for (MmsPart part : parts) for (MmsPart part : getParts())
{ {
builder.append(" "); builder.append(" ");
builder.append(part.toString()); builder.append(part.toString());
@ -84,7 +85,7 @@ public class IncomingMms extends IncomingMessage {
JSONArray partsMetadata = new JSONArray(); JSONArray partsMetadata = new JSONArray();
for (MmsPart part : parts) for (MmsPart part : getParts())
{ {
String formFieldName = "part" + i; String formFieldName = "part" + i;
String text = part.getText(); String text = part.getText();
@ -148,7 +149,7 @@ public class IncomingMms extends IncomingMessage {
@Override @Override
public String getMessageBody() public String getMessageBody()
{ {
for (MmsPart part : parts) for (MmsPart part : getParts())
{ {
if ("text/plain".equals(part.getContentType())) if ("text/plain".equals(part.getContentType()))
{ {
@ -161,11 +162,21 @@ public class IncomingMms extends IncomingMessage {
public Uri getUri() public Uri getUri()
{ {
return Uri.withAppendedPath(App.INCOMING_URI, "mms/" + id); return Uri.withAppendedPath(App.INCOMING_URI, "mms/" + messagingId);
} }
public String getMessageType() public String getMessageType()
{ {
return App.MESSAGE_TYPE_MMS; 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 android.telephony.SmsMessage;
import java.security.InvalidParameterException; import java.security.InvalidParameterException;
import java.util.List; import java.util.List;
import org.envaya.sms.task.ForwarderTask;
public class IncomingSms extends IncomingMessage { public class IncomingSms extends IncomingMessage {
@ -34,12 +33,11 @@ public class IncomingSms extends IncomingMessage {
message = message + smsPart.getMessageBody(); message = message + smsPart.getMessageBody();
} }
} }
// constructor for SMS retrieved from Messaging inbox public IncomingSms(App app)
public IncomingSms(App app, String from, String message, long timestampMillis) { {
super(app, from, timestampMillis); super(app);
this.message = message; }
}
public String getDisplayType() 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.*; import java.util.*;
/* /*
* Utilities for parsing IncomingMms from the MMS content provider tables, * Utilities for parsing MMS and SMS messages from the content provider tables
* as defined by android.provider.Telephony * 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 * core/java/com/google/android/mms/pdu in the base Android framework
* (https://github.com/android/platform_frameworks_base) * (https://github.com/android/platform_frameworks_base)
*/ */
public class MmsUtils public class MessagingUtils
{ {
// constants from android.provider.Telephony // constants from android.provider.Telephony
public static final Uri OBSERVER_URI = Uri.parse("content://mms-sms/"); public static final Uri MMS_INBOX_URI = Uri.parse("content://mms/inbox");
public static final Uri INBOX_URI = Uri.parse("content://mms/inbox"); public static final Uri MMS_PART_URI = Uri.parse("content://mms/part");
public static final Uri 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 // constants from com.google.android.mms.pdu.PduHeaders
private static final int PDU_HEADER_FROM = 0x89; private static final int PDU_HEADER_FROM = 0x89;
private static final int MESSAGE_TYPE_RETRIEVE_CONF = 0x84; 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> seenMmsIds = new HashSet<Long>();
private final Set<Long> seenSentSmsIds = new HashSet<Long>();
private App app; private App app;
private ContentResolver contentResolver; private ContentResolver contentResolver;
public MmsUtils(App app) public MessagingUtils(App app)
{ {
this.app = app; this.app = app;
this.contentResolver = app.getContentResolver(); 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" "_id", "ct", "name", "text", "cid", "_data"
}, "mid = ?", new String[] { "" + id }, null); }, "mid = ?", new String[] { "" + id }, null);
@ -67,7 +70,8 @@ public class MmsUtils
if (name == null || name.length() == 0) 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); part.setName(name);
@ -90,7 +94,7 @@ public class MmsUtils
/* /*
* see com.google.android.mms.pdu.PduPersister.loadAddress * 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"); Uri uri = Uri.parse("content://mms/"+mmsId+"/addr");
@ -112,44 +116,41 @@ public class MmsUtils
return address; return address;
} }
public List<IncomingMms> getMessagesInMmsInbox()
{
return getMessagesInMmsInbox(false);
}
public List<IncomingMms> getMessagesInInbox() public synchronized List<IncomingMms> getMessagesInMmsInbox(boolean newMessagesOnly)
{ {
// the M-Retrieve.conf messages are the 'actual' MMS messages // the M-Retrieve.conf messages are the 'actual' MMS messages
String m_type = "" + MESSAGE_TYPE_RETRIEVE_CONF; 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"}, 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>(); List<IncomingMms> messages = new ArrayList<IncomingMms>();
while (c.moveToNext()) while (c.moveToNext())
{ {
long id = c.getLong(0); long id = c.getLong(0);
long date = c.getLong(2);
String from = getSenderNumber(id); if (newMessagesOnly && seenMmsIds.contains(id))
if (from == null)
{ {
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; continue;
} }
long date = c.getLong(2);
IncomingMms mms = new IncomingMms(app, IncomingMms mms = new IncomingMms(app,
from,
date * 1000, // MMS timestamp is in seconds for some reason, date * 1000, // MMS timestamp is in seconds for some reason,
// while everything else is in ms // while everything else is in ms
id); id);
mms.setContentLocation(c.getString(1));
for (MmsPart part : getMmsParts(id))
{
mms.addPart(part);
}
messages.add(mms); messages.add(mms);
} }
c.close(); c.close();
@ -157,9 +158,9 @@ public class MmsUtils
return messages; 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); Uri uri = Uri.parse("content://mms/inbox/" + id);
int res = contentResolver.delete(uri, null, null); int res = contentResolver.delete(uri, null, null);
@ -181,15 +182,53 @@ public class MmsUtils
return res > 0; 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); seenMmsIds.add(id);
} }
public synchronized boolean isNewMms(IncomingMms mms) public synchronized List<IncomingSms> getSentSmsMessages()
{ {
long id = mms.getId(); return getSentSmsMessages(false);
return !seenMmsIds.contains(id);
} }
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.Context;
import android.content.Intent; import android.content.Intent;
import android.net.Uri; import android.net.Uri;
import android.os.SystemClock;
import java.util.Collection; import java.util.Collection;
import java.util.Comparator; import java.util.Comparator;
import java.util.HashMap; import java.util.HashMap;
@ -18,7 +17,6 @@ import java.util.Queue;
import java.util.Set; import java.util.Set;
import org.apache.http.message.BasicNameValuePair; import org.apache.http.message.BasicNameValuePair;
import org.envaya.sms.receiver.DequeueOutgoingMessageReceiver; import org.envaya.sms.receiver.DequeueOutgoingMessageReceiver;
import org.envaya.sms.receiver.OutgoingMessagePoller;
import org.envaya.sms.task.HttpTask; import org.envaya.sms.task.HttpTask;
public class Outbox { public class Outbox {
@ -81,7 +79,7 @@ public class Outbox {
else { else {
logMessage = "queued"; logMessage = "queued";
} }
String smsDesc = sms.getLogName(); String smsDesc = sms.getDisplayType();
if (serverId != null) { if (serverId != null) {
app.log("Notifying server " + smsDesc + " " + logMessage); app.log("Notifying server " + smsDesc + " " + logMessage);
@ -115,31 +113,33 @@ public class Outbox {
return outgoingMessages.get(uri); 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); outgoingMessages.remove(uri);
addRecentSentMessage(sms); addRecentSentMessage(message);
notifyChanged(); app.getDatabaseHelper().deletePendingMessage(message);
notifyChanged();
numSendingOutgoingMessages--; numSendingOutgoingMessages--;
maybeDequeueMessage(); 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); recentSentMessageUris.add(uri);
recentSentMessageUriOrder.add(uri); recentSentMessageUriOrder.add(uri);
@ -152,21 +152,23 @@ 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 else
{ {
sms.setProcessingState(OutgoingMessage.ProcessingState.None); message.setProcessingState(OutgoingMessage.ProcessingState.None);
} }
notifyChanged(); notifyChanged();
notifyMessageStatus(sms, App.STATUS_FAILED, error); notifyMessageStatus(message, App.STATUS_FAILED, error);
app.getDatabaseHelper().deletePendingMessage(message);
numSendingOutgoingMessages--; numSendingOutgoingMessages--;
maybeDequeueMessage(); maybeDequeueMessage();
} }
@ -185,18 +187,21 @@ public class Outbox {
Uri uri = message.getUri(); Uri uri = message.getUri();
if (outgoingMessages.containsKey(uri)) { if (outgoingMessages.containsKey(uri)) {
app.debug("Duplicate outgoing " + message.getLogName() + ", skipping"); app.debug("Duplicate outgoing " + message.getDisplayType() + ", skipping");
return; return;
} }
if (recentSentMessageUris.contains(uri)) 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, ""); notifyMessageStatus(message, App.STATUS_SENT, "");
return; return;
} }
outgoingMessages.put(uri, message); outgoingMessages.put(uri, message);
app.getDatabaseHelper().insertPendingMessage(message);
enqueueMessage(message); enqueueMessage(message);
} }
@ -213,6 +218,8 @@ public class Outbox {
numSendingOutgoingMessages--; numSendingOutgoingMessages--;
} }
app.getDatabaseHelper().deletePendingMessage(message);
notifyMessageStatus(message, App.STATUS_CANCELLED, notifyMessageStatus(message, App.STATUS_CANCELLED,
"deleted by user"); "deleted by user");
app.log(message.getDescription() + " deleted"); app.log(message.getDescription() + " deleted");

View File

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

View File

@ -11,9 +11,9 @@ public class OutgoingSms extends OutgoingMessage {
super(app); super(app);
} }
public String getLogName() public String getMessageType()
{ {
return "SMS"; return App.MESSAGE_TYPE_SMS;
} }
private ArrayList<String> _bodyParts; private ArrayList<String> _bodyParts;
@ -65,7 +65,7 @@ public class OutgoingSms extends OutgoingMessage {
if (numRetries == 0) if (numRetries == 0)
{ {
app.log("Sending " + getDescription()); app.log("Sending " + getDisplayType());
} }
else else
{ {
@ -105,9 +105,16 @@ public class OutgoingSms extends OutgoingMessage {
if (!app.isForwardablePhoneNumber(to)) if (!app.isForwardablePhoneNumber(to))
{ {
// this is mostly to prevent accidentally sending real messages to if (app.isTestMode() && app.autoAddTestNumber())
// random people while testing... {
throw new ValidationException("Destination address is not allowed"); 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(); String messageBody = getMessageBody();

View File

@ -14,12 +14,29 @@ public abstract class QueuedMessage
protected int numRetries = 0; protected int numRetries = 0;
protected Date dateCreated = new Date(); protected Date dateCreated = new Date();
public App app; 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) public QueuedMessage(App app)
{ {
this.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() public Date getDateCreated()
{ {
return dateCreated; return dateCreated;

View File

@ -1,18 +1,25 @@
package org.envaya.sms; package org.envaya.sms;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; 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.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException; import javax.xml.parsers.ParserConfigurationException;
import org.apache.http.HttpResponse; import org.apache.http.HttpResponse;
import org.json.JSONException;
import org.json.JSONObject;
import org.w3c.dom.Document; import org.w3c.dom.Document;
import org.w3c.dom.Element; import org.w3c.dom.Element;
import org.w3c.dom.NodeList; import org.w3c.dom.NodeList;
import org.xml.sax.SAXException; import org.xml.sax.SAXException;
public class XmlUtils { public class XmlUtils {
public static Document parseResponse(HttpResponse response) public static Document parseResponse(HttpResponse response)
throws IOException, ParserConfigurationException, SAXException { throws IOException, ParserConfigurationException, SAXException {
InputStream responseStream = response.getEntity().getContent(); InputStream responseStream = response.getEntity().getContent();
@ -38,4 +45,53 @@ public class XmlUtils {
} }
return null; 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(); String action = intent.getAction();
boolean isInstalled = !"android.intent.action.PACKAGE_REMOVED".equals(action);
String packageName = intent.getData().getSchemeSpecificPart(); String packageName = intent.getData().getSchemeSpecificPart();
if (packageName != null) 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. * limitations under the License.
*/ */
package org.envaya.sms; package org.envaya.sms.service;
import android.app.Notification; import android.app.Notification;
import android.app.NotificationManager; import android.app.NotificationManager;
@ -23,10 +23,12 @@ import android.app.Service;
import android.content.Intent; import android.content.Intent;
import android.os.IBinder; import android.os.IBinder;
import android.util.Log; import android.util.Log;
import org.envaya.sms.App;
import org.envaya.sms.R;
import java.lang.reflect.InvocationTargetException; import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import org.envaya.sms.ui.LogView; import org.envaya.sms.ui.Main;
/* /*
* Service running in foreground to make sure App instance stays * Service running in foreground to make sure App instance stays
@ -159,13 +161,12 @@ public class ForegroundService extends Service {
System.currentTimeMillis()); System.currentTimeMillis());
PendingIntent contentIntent = PendingIntent.getActivity(this, 0, PendingIntent contentIntent = PendingIntent.getActivity(this, 0,
new Intent(this, LogView.class), 0); new Intent(this, Main.class), 0);
notification.setLatestEventInfo(this, CharSequence info = getText(R.string.running);
"EnvayaSMS running", notification.setLatestEventInfo(this, info, text, contentIntent);
text, contentIntent);
startForegroundCompat(R.string.service_started, notification); startForegroundCompat(R.string.service_started, notification);
} }
else else
{ {

View File

@ -3,6 +3,8 @@ package org.envaya.sms.task;
import android.os.AsyncTask; import android.os.AsyncTask;
import android.os.Build; import android.os.Build;
import org.envaya.sms.App; import org.envaya.sms.App;
import org.envaya.sms.JsonUtils;
import org.envaya.sms.XmlUtils;
import java.io.IOException; import java.io.IOException;
import java.nio.charset.Charset; import java.nio.charset.Charset;
import java.util.ArrayList; 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.MultipartEntity;
import org.apache.http.entity.mime.content.StringBody; import org.apache.http.entity.mime.content.StringBody;
import org.apache.http.message.BasicNameValuePair; 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> { public class BaseHttpTask extends AsyncTask<String, Void, HttpResponse> {
@ -34,6 +39,8 @@ public class BaseHttpTask extends AsyncTask<String, Void, HttpResponse> {
this.url = url; this.url = url;
this.app = app; this.app = app;
params = new ArrayList<BasicNameValuePair>(Arrays.asList(paramsArr)); params = new ArrayList<BasicNameValuePair>(Arrays.asList(paramsArr));
params.add(new BasicNameValuePair("version", "" + app.getPackageInfo().versionCode));
} }
public void addParam(String name, String value) 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 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) if (useMultipartPost)
{ {
@ -83,6 +90,7 @@ public class BaseHttpTask extends AsyncTask<String, Void, HttpResponse> {
try try
{ {
post = makeHttpPost(); post = makeHttpPost();
HttpClient client = app.getHttpClient(); HttpClient client = app.getHttpClient();
return client.execute(post); return client.execute(post);
} }
@ -97,9 +105,10 @@ public class BaseHttpTask extends AsyncTask<String, Void, HttpResponse> {
if ((ex instanceof IOException) if ((ex instanceof IOException)
&& message != null && message.equals("Connection already shutdown")) && message != null && message.equals("Connection already shutdown"))
{ {
//app.log("Retrying request"); // app.log("Retrying request");
post = makeHttpPost(); post = makeHttpPost();
HttpClient client = app.getHttpClient(); HttpClient client = app.getHttpClient();
return client.execute(post); return client.execute(post);
} }
} }
@ -111,10 +120,35 @@ public class BaseHttpTask extends AsyncTask<String, Void, HttpResponse> {
return null; 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 @Override
@ -123,24 +157,13 @@ public class BaseHttpTask extends AsyncTask<String, Void, HttpResponse> {
{ {
try try
{ {
int statusCode = response.getStatusLine().getStatusCode(); 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 (statusCode == 200)
{ {
if (validContentType) handleResponse(response);
{
handleResponse(response);
}
else
{
throw new Exception("Invalid response type " + contentType);
}
} }
else if (statusCode >= 400 && statusCode <= 499 && validContentType) else if (statusCode >= 400 && statusCode <= 499)
{ {
handleErrorResponse(response); handleErrorResponse(response);
handleFailure(); handleFailure();
@ -174,11 +197,6 @@ public class BaseHttpTask extends AsyncTask<String, Void, HttpResponse> {
protected void handleResponse(HttpResponse response) throws Exception protected void handleResponse(HttpResponse response) throws Exception
{ {
// if we get a valid server response after a connectivity error, then forward any pending messages
if (app.hasConnectivityError())
{
app.onConnectivityRestored();
}
} }
protected void handleErrorResponse(HttpResponse response) throws Exception protected void handleErrorResponse(HttpResponse response) throws Exception

View File

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

View File

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

View File

@ -4,32 +4,31 @@
*/ */
package org.envaya.sms.task; package org.envaya.sms.task;
import org.envaya.sms.XmlUtils;
import org.envaya.sms.OutgoingSms;
import org.envaya.sms.OutgoingMessage; import org.envaya.sms.OutgoingMessage;
import org.envaya.sms.App; import org.envaya.sms.JsonUtils;
import org.envaya.sms.Base64Coder; 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.ConnectivityManager;
import android.net.NetworkInfo; import android.net.NetworkInfo;
import android.preference.PreferenceManager;
import java.io.IOException; import java.io.IOException;
import java.io.UnsupportedEncodingException; import java.io.UnsupportedEncodingException;
import java.net.SocketTimeoutException; import java.net.SocketTimeoutException;
import java.net.UnknownHostException; import java.net.UnknownHostException;
import java.security.MessageDigest; import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.Comparator; import java.util.Comparator;
import java.util.List;
import javax.xml.parsers.ParserConfigurationException; import javax.xml.parsers.ParserConfigurationException;
import org.apache.commons.io.IOUtils;
import org.apache.http.HttpResponse; import org.apache.http.HttpResponse;
import org.apache.http.client.methods.HttpPost; import org.apache.http.client.methods.HttpPost;
import org.apache.http.message.BasicNameValuePair; import org.apache.http.message.BasicNameValuePair;
import org.json.JSONException;
import org.json.JSONObject;
import org.w3c.dom.Document; import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException; import org.xml.sax.SAXException;
public class HttpTask extends BaseHttpTask { public class HttpTask extends BaseHttpTask {
@ -97,11 +96,37 @@ public class HttpTask extends BaseHttpTask {
return null; return null;
} }
logEntries = app.getNewLogEntries(); logEntries = app.getNewLogEntries();
params.add(new BasicNameValuePair("version", "" + app.getPackageInfo().versionCode));
params.add(new BasicNameValuePair("phone_number", app.getPhoneNumber())); params.add(new BasicNameValuePair("phone_number", app.getPhoneNumber()));
params.add(new BasicNameValuePair("phone_id", app.getPhoneID()));
params.add(new BasicNameValuePair("phone_token", app.getPhoneToken()));
params.add(new BasicNameValuePair("send_limit", "" + app.getOutgoingMessageLimit())); 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 cm =
(ConnectivityManager)app.getSystemService(App.CONNECTIVITY_SERVICE); (ConnectivityManager)app.getSystemService(App.CONNECTIVITY_SERVICE);
@ -133,56 +158,29 @@ public class HttpTask extends BaseHttpTask {
protected String getDefaultToAddress() protected String getDefaultToAddress()
{ {
return ""; return "";
}
protected void handleResponseJSON(JSONObject json)
throws JSONException
{
JsonUtils.processEvents(json, app, getDefaultToAddress());
} }
protected List<OutgoingMessage> parseResponseXML(HttpResponse response) protected void handleResponseXML(Document xml)
throws IOException, ParserConfigurationException, SAXException throws IOException, ParserConfigurationException, SAXException
{ {
List<OutgoingMessage> messages = new ArrayList<OutgoingMessage>(); for (OutgoingMessage message : XmlUtils.getMessagesList(xml, app, getDefaultToAddress()))
Document xml = XmlUtils.parseResponse(response);
Element messagesElement = (Element) xml.getElementsByTagName("messages").item(0);
if (messagesElement != null)
{ {
NodeList messageNodes = messagesElement.getChildNodes(); app.outbox.sendMessage(message);
int numNodes = messageNodes.getLength(); }
for (int i = 0; i < numNodes; i++) }
{
Element messageElement = (Element) messageNodes.item(i); protected void handleUnknownContentType(String contentType)
throws Exception
OutgoingMessage message = new OutgoingSms(app); {
// old server API only mandated valid content type for action=outgoing
message.setFrom(app.getPhoneNumber()); app.log("Warning: Unknown response type " + contentType);
}
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);
}
}
return messages;
}
@Override @Override
protected void handleFailure() protected void handleFailure()
@ -221,16 +219,38 @@ public class HttpTask extends BaseHttpTask {
@Override @Override
public void handleErrorResponse(HttpResponse response) throws Exception public void handleErrorResponse(HttpResponse response) throws Exception
{ {
Document xml = XmlUtils.parseResponse(response); app.log(getErrorText(response));
String error = XmlUtils.getErrorText(xml); }
if (error != null)
@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 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.HttpResponse;
import org.apache.http.message.BasicNameValuePair; import org.apache.http.message.BasicNameValuePair;
import org.envaya.sms.App; import org.envaya.sms.App;
import org.envaya.sms.OutgoingMessage;
public class PollerTask extends HttpTask { public class PollerTask extends HttpTask {
@ -12,12 +11,6 @@ public class PollerTask extends HttpTask {
super(app, new BasicNameValuePair("action", App.ACTION_OUTGOING)); super(app, new BasicNameValuePair("action", App.ACTION_OUTGOING));
} }
@Override
public boolean isValidContentType(String contentType)
{
return contentType.startsWith("text/xml");
}
@Override @Override
protected void onPostExecute(HttpResponse response) { protected void onPostExecute(HttpResponse response) {
super.onPostExecute(response); super.onPostExecute(response);
@ -25,9 +18,9 @@ public class PollerTask extends HttpTask {
} }
@Override @Override
protected void handleResponse(HttpResponse response) throws Exception { protected void handleUnknownContentType(String contentType)
for (OutgoingMessage reply : parseResponseXML(response)) { throws Exception
app.outbox.sendMessage(reply); {
} 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(); 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 />"; + "Menu icons cc/by www.androidicons.com<br /><br />";
help.setText(Html.fromHtml(html)); help.setText(Html.fromHtml(html));

View File

@ -2,21 +2,28 @@ package org.envaya.sms.ui;
import org.envaya.sms.task.HttpTask; import org.envaya.sms.task.HttpTask;
import android.app.Activity; import android.app.Activity;
import android.app.AlertDialog;
import android.content.*; import android.content.*;
import android.content.DialogInterface.OnCancelListener;
import android.content.DialogInterface.OnClickListener;
import android.net.Uri; import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.preference.PreferenceManager; import android.preference.PreferenceManager;
import android.text.Html;
import android.text.method.LinkMovementMethod; import android.text.method.LinkMovementMethod;
import android.view.Menu; import android.view.Menu;
import android.view.MenuInflater; import android.view.MenuInflater;
import android.view.MenuItem; import android.view.MenuItem;
import android.view.View; import android.view.View;
import android.widget.Button;
import android.widget.ScrollView; import android.widget.ScrollView;
import android.widget.TextView; import android.widget.TextView;
import org.apache.http.HttpResponse; import org.apache.http.HttpResponse;
import org.apache.http.message.BasicNameValuePair; import org.apache.http.message.BasicNameValuePair;
import org.envaya.sms.App; import org.envaya.sms.App;
import org.envaya.sms.R; import org.envaya.sms.R;
import java.util.ArrayList;
import java.util.List;
public class LogView extends Activity { 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 ScrollView scrollView;
private TextView info; private TextView info;
private TextView log;
private TextView heading;
private class TestTask extends HttpTask private class TestTask extends HttpTask
{ {
@ -47,14 +71,31 @@ public class LogView extends Activity {
private int lastLogEpoch = -1; 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(); int logEpoch = app.getLogEpoch();
CharSequence displayedLog = app.getDisplayedLog(); 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(); int afterLen = displayedLog.length();
if (beforeLen == afterLen) if (beforeLen == afterLen)
@ -62,11 +103,11 @@ public class LogView extends Activity {
return; return;
} }
info.append(displayedLog, beforeLen, afterLen); log.append(displayedLog, beforeLen, afterLen);
} }
else else
{ {
info.setText(displayedLog); log.setText(displayedLog);
lastLogEpoch = logEpoch; lastLogEpoch = logEpoch;
} }
@ -85,24 +126,227 @@ public class LogView extends Activity {
setContentView(R.layout.log_view); setContentView(R.layout.log_view);
PreferenceManager.setDefaultValues(this, R.xml.prefs, false); PreferenceManager.setDefaultValues(this, R.xml.prefs, false);
scrollView = (ScrollView) this.findViewById(R.id.info_scroll); scrollView = (ScrollView) this.findViewById(R.id.log_scroll);
info = (TextView) this.findViewById(R.id.info);
info.setMovementMethod(LinkMovementMethod.getInstance()); heading = (TextView) this.findViewById(R.id.heading);
info = (TextView) this.findViewById(R.id.info);
//info.setMovementMethod(new ScrollingMovementMethod()); updateInfo();
log = (TextView) this.findViewById(R.id.log);
log.setMovementMethod(LinkMovementMethod.getInstance());
updateUpgradeButton();
updateLogView(); updateLogView();
IntentFilter logReceiverFilter = new IntentFilter(); registerReceiver(logReceiver, new IntentFilter(App.LOG_CHANGED_INTENT));
logReceiverFilter.addAction(App.LOG_CHANGED_INTENT); registerReceiver(settingsReceiver, new IntentFilter(App.SETTINGS_CHANGED_INTENT));
registerReceiver(logReceiver, logReceiverFilter); 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 @Override
public void onDestroy() public void onDestroy()
{ {
this.unregisterReceiver(logReceiver); unregisterReceiver(logReceiver);
unregisterReceiver(settingsReceiver);
unregisterReceiver(expansionPacksReceiver);
super.onDestroy(); super.onDestroy();
} }
@ -119,8 +363,8 @@ public class LogView extends Activity {
case R.id.retry_now: case R.id.retry_now:
app.retryStuckMessages(); app.retryStuckMessages();
return true; return true;
case R.id.forward_inbox: case R.id.forward_saved:
startActivity(new Intent(this, MessagingInbox.class)); startActivity(new Intent(this, MessagingSmsInbox.class));
return true; return true;
case R.id.pending: case R.id.pending:
startActivity(new Intent(this, PendingMessages.class)); 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.app.ListActivity;
import android.content.DialogInterface; import android.content.DialogInterface;
import android.content.DialogInterface.OnClickListener; import android.content.DialogInterface.OnClickListener;
import android.database.Cursor; import android.content.Intent;
import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.view.Menu; import android.view.Menu;
import android.view.MenuInflater; import android.view.MenuInflater;
import android.view.MenuItem; import android.view.MenuItem;
import android.view.View; import android.view.View;
import android.widget.AdapterView; import android.widget.*;
import android.widget.AdapterView.OnItemClickListener; import android.widget.AdapterView.OnItemClickListener;
import android.widget.ListView; import android.widget.AdapterView.OnItemSelectedListener;
import android.widget.SimpleCursorAdapter;
import android.widget.Toast;
import org.envaya.sms.App; import org.envaya.sms.App;
import org.envaya.sms.IncomingMessage; import org.envaya.sms.IncomingMessage;
import org.envaya.sms.IncomingSms;
import org.envaya.sms.R; 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. */ /** Called when the activity is first created. */
@Override @Override
@ -37,24 +36,59 @@ public class MessagingInbox extends ListActivity {
app = (App) getApplication(); app = (App) getApplication();
setContentView(R.layout.inbox); setContentView(R.layout.inbox);
final String[] inboxTypeClasses = new String[] {
"org.envaya.sms.ui.MessagingSmsInbox",
"org.envaya.sms.ui.MessagingMmsInbox",
"org.envaya.sms.ui.MessagingSentSms",
};
final String[] inboxTypeNames = new String[] {
"SMS Inbox",
"MMS Inbox",
"Sent SMS"
};
// undocumented API; see Spinner spinner = (Spinner) findViewById(R.id.inbox_selector);
// core/java/android/provider/Telephony.java
Uri inboxUri = Uri.parse("content://sms/inbox"); 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);
cur = getContentResolver().query(inboxUri, final String className = this.getClass().getCanonicalName();
new String[] { "_id", "address", "body", "date" }, null, null, int classIndex = Arrays.asList(inboxTypeClasses).indexOf(className);
"_id desc limit 50"); if (classIndex != -1)
{
spinner.setSelection(classIndex);
}
SimpleCursorAdapter adapter = new SimpleCursorAdapter(this, spinner.setOnItemSelectedListener(new OnItemSelectedListener() {
R.layout.inbox_item, public void onItemSelected(AdapterView<?> parent,
cur, View view, int pos, long id) {
new String[] {"address","body"}, String cls = inboxTypeClasses[pos];
new int[] {R.id.inbox_address, R.id.inbox_body});
if (!className.equals(cls))
{
try
{
finish();
startActivity(new Intent(app, Class.forName(cls)));
}
catch (ClassNotFoundException ex)
{
app.logError(ex);
}
}
}
public void onNothingSelected(AdapterView parent) {
}
});
setListAdapter(adapter);
initListAdapter();
ListView listView = getListView(); ListView listView = getListView();
listView.setOnItemClickListener(new OnItemClickListener() { listView.setOnItemClickListener(new OnItemClickListener() {
@ -63,9 +97,9 @@ public class MessagingInbox extends ListActivity {
{ {
final IncomingMessage message = getMessageAtPosition(position); 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()) .setTitle(message.getDescription())
.setItems(options, new OnClickListener() { .setItems(options, new OnClickListener() {
public void onClick(DialogInterface dialog, int which) public void onClick(DialogInterface dialog, int which)
@ -73,44 +107,29 @@ public class MessagingInbox extends ListActivity {
if (which == 0) if (which == 0)
{ {
app.inbox.forwardMessage(message); app.inbox.forwardMessage(message);
showToast("Forwarding " + message.getDescription()); showToast("Forwarding " + message.getDescription() + " to server");
} }
dialog.dismiss(); dialog.dismiss();
} }
}) })
.show(); .show();
} }
}); });
} }
public void showToast(String text) public void showToast(String text)
{ {
Toast.makeText(this, text, Toast.LENGTH_SHORT).show(); 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() { public void forwardAllClicked() {
final int count = cur.getCount(); final int count = getMessageCount();
for (int i = 0; i < count; ++i) for (int i = 0; i < count; ++i)
{ {
app.inbox.forwardMessage(getMessageAtPosition(i)); app.inbox.forwardMessage(getMessageAtPosition(i));
} }
finish(); finish();
} }
@Override @Override
@ -137,9 +156,9 @@ public class MessagingInbox extends ListActivity {
public boolean onPrepareOptionsMenu(Menu menu) { public boolean onPrepareOptionsMenu(Menu menu) {
MenuItem forwardItem = menu.findItem(R.id.forward_all); MenuItem forwardItem = menu.findItem(R.id.forward_all);
int numMessages = cur.getCount(); int numMessages = getMessageCount();
forwardItem.setEnabled(numMessages > 0); forwardItem.setEnabled(numMessages > 0);
forwardItem.setTitle("Forward All (" + numMessages + ")"); forwardItem.setTitle("Forward all to server (" + numMessages + ")");
return true; 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; 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 LayoutInflater inflater = (LayoutInflater)getSystemService(Context.LAYOUT_INFLATER_SERVICE);
final DateFormat longFormat = new SimpleDateFormat("dd MMM hh:mm:ss"); final DateFormat longFormat = new SimpleDateFormat("dd MMM hh:mm:ss");

View File

@ -1,15 +1,16 @@
package org.envaya.sms.ui; package org.envaya.sms.ui;
import android.content.SharedPreferences; 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.content.SharedPreferences.OnSharedPreferenceChangeListener;
import android.os.Bundle; import android.os.Bundle;
import android.preference.EditTextPreference; import android.preference.*;
import android.preference.ListPreference;
import android.preference.Preference;
import android.preference.PreferenceActivity;
import android.preference.PreferenceScreen;
import android.provider.Settings; import android.provider.Settings;
import android.provider.Settings.SettingNotFoundException; import android.provider.Settings.SettingNotFoundException;
import android.text.method.PasswordTransformationMethod;
import android.view.Menu; import android.view.Menu;
import org.envaya.sms.App; import org.envaya.sms.App;
import org.envaya.sms.R; import org.envaya.sms.R;
@ -18,6 +19,14 @@ public class Prefs extends PreferenceActivity implements OnSharedPreferenceChang
private App app; 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 @Override
public void onCreate(Bundle savedInstanceState) { public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
@ -32,8 +41,19 @@ public class Prefs extends PreferenceActivity implements OnSharedPreferenceChang
{ {
updatePrefSummary(screen.getPreference(i)); 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 @Override
protected void onResume(){ protected void onResume(){
super.onResume(); super.onResume();
@ -50,11 +70,21 @@ public class Prefs extends PreferenceActivity implements OnSharedPreferenceChang
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
if (key.equals("outgoing_interval")) if (key.equals("outgoing_interval"))
{ {
app.setOutgoingMessageAlarm(); app.setOutgoingMessageAlarm();
} }
else if (key.startsWith("amqp_"))
{
if (app.isAmqpEnabled())
{
app.getAmqpConsumer().startAsync();
}
else
{
app.getAmqpConsumer().stopAsync();
}
}
else if (key.equals("wifi_sleep_policy")) else if (key.equals("wifi_sleep_policy"))
{ {
int value; int value;
@ -107,15 +137,21 @@ public class Prefs extends PreferenceActivity implements OnSharedPreferenceChang
} }
else if (key.equals("enabled")) 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(); app.enabledChanged();
} }
sendBroadcast(new Intent(App.SETTINGS_CHANGED_INTENT));
updatePrefSummary(findPreference(key)); updatePrefSummary(findPreference(key));
} }
private void updatePrefSummary(Preference p) private void updatePrefSummary(Preference p)
{ {
if (p == null)
{
return;
}
String key = p.getKey(); String key = p.getKey();
if ("wifi_sleep_policy".equals(key)) if ("wifi_sleep_policy".equals(key))
@ -144,11 +180,32 @@ public class Prefs extends PreferenceActivity implements OnSharedPreferenceChang
p.setSummary("Wi-Fi will stay connected when the phone sleeps"); p.setSummary("Wi-Fi will stay connected when the phone sleeps");
break; 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)) else if ("help".equals(key))
{ {
p.setSummary(app.getPackageInfo().versionName); 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) { else if (p instanceof ListPreference) {
p.setSummary(((ListPreference)p).getEntry()); p.setSummary(((ListPreference)p).getEntry());
} }
@ -160,7 +217,7 @@ public class Prefs extends PreferenceActivity implements OnSharedPreferenceChang
{ {
p.setSummary("(not set)"); p.setSummary("(not set)");
} }
else if (p.getKey().equals("password")) else if (textPref.getEditText().getTransformationMethod() instanceof PasswordTransformationMethod)
{ {
p.setSummary("********"); p.setSummary("********");
} }

View File

@ -8,6 +8,7 @@ import android.os.Bundle;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.widget.AdapterView; import android.widget.AdapterView;
import android.widget.CheckBox;
import android.widget.AdapterView.OnItemClickListener; import android.widget.AdapterView.OnItemClickListener;
import android.widget.ArrayAdapter; import android.widget.ArrayAdapter;
import android.widget.EditText; import android.widget.EditText;
@ -20,6 +21,8 @@ public class TestPhoneNumbers extends ListActivity {
private App app; private App app;
private CheckBox autoAddOutgoing;
@Override @Override
public void onCreate(Bundle icicle) { public void onCreate(Bundle icicle) {
super.onCreate(icicle); super.onCreate(icicle);
@ -27,6 +30,9 @@ public class TestPhoneNumbers extends ListActivity {
app = (App)getApplication(); app = (App)getApplication();
autoAddOutgoing = (CheckBox)findViewById(R.id.auto_add_outgoing);
autoAddOutgoing.setChecked(app.autoAddTestNumber());
ListView lv = getListView(); ListView lv = getListView();
lv.setOnItemClickListener(new OnItemClickListener() { lv.setOnItemClickListener(new OnItemClickListener() {
public void onItemClick(AdapterView<?> parent, View view, public void onItemClick(AdapterView<?> parent, View view,
@ -63,6 +69,13 @@ public class TestPhoneNumbers extends ListActivity {
updateTestPhoneNumbers(); 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() public void updateTestPhoneNumbers()
{ {
String[] senders = app.getTestPhoneNumbers().toArray(new String[]{}); String[] senders = app.getTestPhoneNumbers().toArray(new String[]{});