diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..315f6f8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +local.properties +bin +gen +nbandroid \ No newline at end of file diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 02b3179..3b780c0 100755 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -1,8 +1,8 @@ + android:versionCode="18" + android:versionName="2.0.5"> @@ -10,6 +10,7 @@ + @@ -22,13 +23,14 @@ - + + - + @@ -81,6 +83,9 @@ + + + diff --git a/ant.properties b/ant.properties new file mode 100644 index 0000000..ee52d86 --- /dev/null +++ b/ant.properties @@ -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. + diff --git a/build.properties b/build.properties index ee52d86..e69de29 100755 --- a/build.properties +++ b/build.properties @@ -1,17 +0,0 @@ -# This file is used to override default values used by the Ant build system. -# -# This file must be checked in Version Control Systems, as it is -# integral to the build system of your project. - -# This file is only used by the Ant script. - -# You can use this to override default values such as -# 'source.dir' for the location of your java source folder and -# 'out.dir' for the location of your output folder. - -# You can also use it define how the release builds are signed by declaring -# the following properties: -# 'key.store' for the location of your keystore and -# 'key.alias' for the name of the key to use. -# The password will be asked during the build when you use the 'release' target. - diff --git a/build.xml b/build.xml index a2ab2d1..3981f58 100755 --- a/build.xml +++ b/build.xml @@ -1,79 +1,85 @@ - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/default.properties b/default.properties index 9d6f70d..e69de29 100644 --- a/default.properties +++ b/default.properties @@ -1,13 +0,0 @@ -# This file is automatically generated by Android Tools. -# Do not modify this file -- YOUR CHANGES WILL BE ERASED! -# -# This file must be checked in Version Control Systems. -# -# To customize properties used by the Ant build system use, -# "build.properties", and override values to adapt the script to your -# project structure. - -# Project target. -target=android-4 -# Indicates whether an apk should be generated for each density. -split.density=false diff --git a/project.properties b/project.properties new file mode 100644 index 0000000..19f2f1b --- /dev/null +++ b/project.properties @@ -0,0 +1,13 @@ +# This file is automatically generated by Android Tools. +# Do not modify this file -- YOUR CHANGES WILL BE ERASED! +# +# This file must be checked in Version Control Systems. +# +# To customize properties used by the Ant build system use, +# "ant.properties", and override values to adapt the script to your +# project structure. + +# Indicates whether an apk should be generated for each density. +split.density=false +# Project target. +target=android-4 diff --git a/res/layout/main.xml b/res/layout/log_view.xml old mode 100755 new mode 100644 similarity index 100% rename from res/layout/main.xml rename to res/layout/log_view.xml diff --git a/res/xml/prefs.xml b/res/xml/prefs.xml index f0815ad..6c87fcd 100755 --- a/res/xml/prefs.xml +++ b/res/xml/prefs.xml @@ -45,6 +45,13 @@ android:summaryOn="Incoming SMS will be stored in Messaging inbox" > + + - + \n"; + echo ""; + echo ""; + echo EnvayaSMS::escape($message); + echo ""; + echo ""; + return ob_get_clean(); + } + + static function get_success_xml() + { + ob_start(); + echo "\n"; + echo ""; + return ob_get_clean(); + } } class EnvayaSMS_Request @@ -56,12 +82,28 @@ class EnvayaSMS_Request public $version; public $phone_number; public $log; + + public $version_name; + public $sdk_int; + public $manufacturer; + public $model; + public $network; function __construct() { $this->version = $_POST['version']; $this->phone_number = $_POST['phone_number']; - $this->log = @$_POST['log']; + $this->log = $_POST['log']; + $this->network = @$_POST['network']; + + if (preg_match('#/(?P[\w\.\-]+) \(Android; SDK (?P\d+); (?P[^;]*); (?P[^\)]*)\)#', + @$_SERVER['HTTP_USER_AGENT'], $matches)) + { + $this->version_name = $matches['version_name']; + $this->sdk_int = $matches['sdk_int']; + $this->manufacturer = $matches['manufacturer']; + $this->model = $matches['model']; + } } function get_action() @@ -124,21 +166,24 @@ class EnvayaSMS_Request //error_log("Signed data: '$input'"); return base64_encode(sha1($input, true)); - } + } static function get_messages_xml($messages) { ob_start(); echo "\n"; + echo ""; echo ""; 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 "".EnvayaSMS::escape($message->message).""; + echo "<$type$id$to$priority>".EnvayaSMS::escape($message->message).""; } echo ""; + echo ""; return ob_get_clean(); } } @@ -149,6 +194,7 @@ class EnvayaSMS_OutgoingMessage public $to; // destination phone number public $message; // content of SMS message public $priority; // integer priority, higher numbers will be sent first + public $type; // EnvayaSMS::MESSAGE_TYPE_* value (default sms) } class EnvayaSMS_Action @@ -194,15 +240,17 @@ class EnvayaSMS_Action_Incoming extends EnvayaSMS_Action public $message_type; // EnvayaSMS::MESSAGE_TYPE_MMS or EnvayaSMS::MESSAGE_TYPE_SMS public $mms_parts; // array of EnvayaSMS_MMS_Part instances public $timestamp; // timestamp of incoming message (added in version 12) + public $age; // delay in ms between time when message originally received and when forwarded to server (added in version 18) function __construct($request) { parent::__construct($request); $this->type = EnvayaSMS::ACTION_INCOMING; $this->from = $_POST['from']; - $this->message = $_POST['message']; + $this->message = @$_POST['message']; $this->message_type = $_POST['message_type']; $this->timestamp = @$_POST['timestamp']; + $this->age = @$_POST['age']; if ($this->message_type == EnvayaSMS::MESSAGE_TYPE_MMS) { @@ -247,7 +295,7 @@ class EnvayaSMS_Action_SendStatus extends EnvayaSMS_Action { public $status; // EnvayaSMS::STATUS_* values public $id; // server ID previously used in EnvayaSMS_OutgoingMessage - public $error; // textual descrption of error (if applicable) + public $error; // textual description of error (if applicable) function __construct($request) { diff --git a/server/php/example/mms_parts/.gitignore b/server/php/example/mms_parts/.gitignore new file mode 100644 index 0000000..c96a04f --- /dev/null +++ b/server/php/example/mms_parts/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore \ No newline at end of file diff --git a/server/php/example/www/index.php b/server/php/example/www/index.php index 510895f..5525f6c 100755 --- a/server/php/example/www/index.php +++ b/server/php/example/www/index.php @@ -14,11 +14,13 @@ $phone_number = $request->phone_number; $password = @$PASSWORDS[$phone_number]; +header("Content-Type: text/xml"); + if (!isset($password) || !$request->is_validated($password)) { header("HTTP/1.1 403 Forbidden"); - error_log("Invalid request signature"); - echo "Invalid request signature"; + error_log("Invalid request signature"); + echo EnvayaSMS::get_error_xml("Invalid request signature"); return; } @@ -37,14 +39,30 @@ $action = $request->get_action(); switch ($action->type) { case EnvayaSMS::ACTION_INCOMING: - error_log("Received SMS from {$action->from}"); + $type = strtoupper($action->message_type); + + error_log("Received $type from {$action->from}"); + error_log(" message: {$action->message}"); + + if ($action->message_type == EnvayaSMS::MESSAGE_TYPE_MMS) + { + foreach ($action->mms_parts as $mms_part) + { + $ext_map = array('image/jpeg' => 'jpg', 'image/gif' => 'gif', 'text/plain' => 'txt', 'application/smil' => 'smil'); + $ext = @$ext_map[$mms_part->type] ?: "unk"; + + $filename = "mms_parts/" . uniqid('mms') . ".$ext"; + + copy($mms_part->tmp_name, dirname(__DIR__)."/$filename"); + error_log(" mms part type {$mms_part->type} saved to {$filename}"); + } + } $reply = new EnvayaSMS_OutgoingMessage(); $reply->message = "You said: {$action->message}"; error_log("Sending reply: {$reply->message}"); - header("Content-Type: text/xml"); echo $action->get_response_xml(array($reply)); return; @@ -70,7 +88,6 @@ switch ($action->type) } closedir($dir); - header("Content-Type: text/xml"); echo $action->get_response_xml($messages); return; @@ -81,23 +98,23 @@ switch ($action->type) // delete file with matching id if (preg_match('#^\w+$#', $id) && unlink("$OUTGOING_DIR_NAME/$id.json")) { - echo "OK"; + echo EnvayaSMS::get_success_xml(); } else { header("HTTP/1.1 404 Not Found"); - echo "invalid id"; + echo EnvayaSMS::get_error_xml("Invalid id"); } return; case EnvayaSMS::ACTION_DEVICE_STATUS: error_log("device_status = {$action->status}"); - echo "OK"; + echo EnvayaSMS::get_success_xml(); return; case EnvayaSMS::ACTION_TEST: - echo "OK"; + echo EnvayaSMS::get_success_xml(); return; default: header("HTTP/1.1 404 Not Found"); - echo "Invalid action"; + echo EnvayaSMS::get_error_xml("Invalid action"); return; } \ No newline at end of file diff --git a/src/org/envaya/sms/App.java b/src/org/envaya/sms/App.java index 100601a..c8da1d0 100755 --- a/src/org/envaya/sms/App.java +++ b/src/org/envaya/sms/App.java @@ -14,14 +14,15 @@ import android.net.ConnectivityManager; import android.net.NetworkInfo; import android.net.Uri; import android.net.wifi.WifiManager; +import android.os.AsyncTask; import android.os.Bundle; import android.os.SystemClock; import android.preference.PreferenceManager; +import android.telephony.PhoneStateListener; +import android.telephony.TelephonyManager; import android.text.Html; import android.text.SpannableStringBuilder; import android.util.Log; -import java.io.IOException; -import java.net.InetAddress; import java.text.DateFormat; import java.util.ArrayList; import java.util.Date; @@ -42,9 +43,10 @@ import org.apache.http.params.HttpConnectionParams; import org.apache.http.params.HttpParams; import org.apache.http.params.HttpProtocolParams; import org.envaya.sms.receiver.OutgoingMessagePoller; -import org.envaya.sms.receiver.ReenableWifiReceiver; +import org.envaya.sms.task.CheckConnectivityTask; import org.envaya.sms.task.HttpTask; import org.envaya.sms.task.PollerTask; +import org.apache.http.message.BasicNameValuePair; import org.json.JSONArray; import org.json.JSONException; @@ -59,14 +61,17 @@ public final class App extends Application { public static final String STATUS_QUEUED = "queued"; public static final String STATUS_FAILED = "failed"; public static final String STATUS_SENT = "sent"; + public static final String STATUS_CANCELLED = "cancelled"; public static final String DEVICE_STATUS_POWER_CONNECTED = "power_connected"; public static final String DEVICE_STATUS_POWER_DISCONNECTED = "power_disconnected"; public static final String DEVICE_STATUS_BATTERY_LOW = "battery_low"; public static final String DEVICE_STATUS_BATTERY_OKAY = "battery_okay"; + public static final String DEVICE_STATUS_SEND_LIMIT_EXCEEDED = "send_limit_exceeded"; public static final String MESSAGE_TYPE_MMS = "mms"; public static final String MESSAGE_TYPE_SMS = "sms"; + public static final String MESSAGE_TYPE_CALL = "call"; public static final String LOG_NAME = "EnvayaSMS"; @@ -76,9 +81,9 @@ public final class App extends Application { // signal to PendingMessages activity (if open) that inbox/outbox has changed public static final String INBOX_CHANGED_INTENT = "org.envaya.sms.INBOX_CHANGED"; public static final String OUTBOX_CHANGED_INTENT = "org.envaya.sms.OUTBOX_CHANGED"; - + public static final String QUERY_EXPANSION_PACKS_INTENT = "org.envaya.sms.QUERY_EXPANSION_PACKS"; - public static final String QUERY_EXPANSION_PACKS_EXTRA_PACKAGES = "packages"; + public static final String QUERY_EXPANSION_PACKS_EXTRA_PACKAGES = "packages"; // Interface for sending outgoing messages to expansion packs public static final String OUTGOING_SMS_INTENT_SUFFIX = ".OUTGOING_SMS"; @@ -96,11 +101,13 @@ public final class App extends Application { public static final String STATUS_EXTRA_NUM_PARTS = "num_parts"; public static final int MAX_DISPLAYED_LOG = 8000; - public static final int LOG_TIMESTAMP_INTERVAL = 60000; // ms + public static final int LOG_TIMESTAMP_INTERVAL = 30000; //60000; // ms public static final int HTTP_CONNECTION_TIMEOUT = 10000; // ms public static final int HTTP_SOCKET_TIMEOUT = 60000; // ms + public static final int MESSAGE_SEND_TIMEOUT = 30000; // ms + // Each QueuedMessage is identified within our internal Map by its Uri. // Currently QueuedMessage instances are only available within EnvayaSMS, // (but they could be made available to other applications later via a ContentProvider) @@ -113,7 +120,7 @@ public final class App extends Application { public static final int DISABLE_WIFI_INTERVAL = 3600000; // how often we can automatically failover between wifi/mobile connection - public static final int CONNECTIVITY_FAILOVER_INTERVAL = 1800000; + public static final int CONNECTIVITY_FAILOVER_INTERVAL = 900000; // max per-app outgoing SMS rate used by com.android.internal.telephony.SMSDispatcher // with a slightly longer check period to account for variance in the time difference @@ -145,7 +152,10 @@ public final class App extends Application { private int outgoingMessageCount = -1; private MmsUtils mmsUtils; + private CallListener callListener; + private boolean connectivityError = false; + @Override public void onCreate() { @@ -154,6 +164,8 @@ public final class App extends Application { settings = PreferenceManager.getDefaultSharedPreferences(this); mmsUtils = new MmsUtils(this); + callListener = new CallListener(this); + outgoingMessagePackages.add(getPackageName()); mmsObserver = new MmsObserver(this); @@ -170,39 +182,87 @@ public final class App extends Application { } updateExpansionPacks(); - + configuredChanged(); + } + + public void configuredChanged() + { log(Html.fromHtml( - isEnabled() ? "SMS gateway running." : "SMS gateway disabled.")); + isEnabled() ? "SMS gateway running ("+getDisplayString(getPhoneNumber())+")." + : "SMS gateway disabled.")); + + 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 ignoredNumbers = getIgnoredPhoneNumbers(); + + if (ignoredNumbers.size() > 0 || ignoreShortcodes || ignoreNonNumeric) + { + log("Ignored phone numbers:"); + + if (ignoreShortcodes || ignoreNonNumeric) + { + String ignoreDesc = " "; + if (ignoreShortcodes) + { + ignoreDesc += "all shortcodes"; + } + + if (ignoreShortcodes && ignoreNonNumeric) + { + ignoreDesc += ", "; + } + + if (ignoreNonNumeric) + { + ignoreDesc += "all non-numeric"; + } + log(ignoreDesc); + } + + for (String sender : ignoredNumbers) + { + log(" " + sender); + } + } - log("Server URL is: " + getDisplayString(getServerUrl())); - log("Your phone number is: " + getDisplayString(getPhoneNumber())); - if (isTestMode()) { - log("Test mode is ON"); + log("Test mode: ON"); log("Test phone numbers:"); - + for (String sender : getTestPhoneNumbers()) { log(" " + sender); } } - + + log(Html.fromHtml("To change these settings, click Menu, then Settings.")); + enabledChanged(); - - log(Html.fromHtml("Press Menu to edit settings.")); - } + } public void enabledChanged() { + TelephonyManager telephony = (TelephonyManager) + getSystemService(Context.TELEPHONY_SERVICE); + if (isEnabled()) { - mmsObserver.register(); + mmsObserver.register(); + + telephony.listen(callListener, PhoneStateListener.LISTEN_CALL_STATE); } else { mmsObserver.unregister(); - } + + telephony.listen(callListener, PhoneStateListener.LISTEN_NONE); + } setOutgoingMessageAlarm(); startService(new Intent(this, ForegroundService.class)); @@ -259,6 +319,13 @@ public final class App extends Application { + getOutgoingMessageLimit() + " in 1 hour reached"); log("To increase this limit, install an expansion pack."); + HttpTask task = new HttpTask(this, + new BasicNameValuePair("action", App.ACTION_DEVICE_STATUS), + new BasicNameValuePair("status", App.DEVICE_STATUS_SEND_LIMIT_EXCEEDED) + ); + task.setRetryOnConnectivityError(true); + task.execute(); + return null; } @@ -338,13 +405,14 @@ public final class App extends Application { sendOrderedBroadcast( new Intent(App.QUERY_EXPANSION_PACKS_INTENT), - "android.permission.SEND_SMS", + null, new BroadcastReceiver() { @Override public void onReceive(Context context, Intent resultIntent) { - setExpansionPacks(this.getResultExtras(false) - .getStringArrayList(App.QUERY_EXPANSION_PACKS_EXTRA_PACKAGES)); + Bundle extras = this.getResultExtras(false); + + setExpansionPacks(extras.getStringArrayList(App.QUERY_EXPANSION_PACKS_EXTRA_PACKAGES)); } }, @@ -375,7 +443,7 @@ public final class App extends Application { String serverUrl = getServerUrl(); if (serverUrl.length() > 0) { log("Checking for outgoing messages"); - pollActive = true; + pollActive = true; new PollerTask(this).execute(); } else { log("Can't check outgoing messages; server URL not set"); @@ -402,7 +470,7 @@ public final class App extends Application { if (isEnabled()) { - if (pollSeconds > 0) { + if (pollSeconds > 0) { alarm.setRepeating( AlarmManager.ELAPSED_REALTIME_WAKEUP, SystemClock.elapsedRealtime(), @@ -411,7 +479,7 @@ public final class App extends Application { log("Checking for outgoing messages every " + pollSeconds + " sec"); } else { log("Not checking for outgoing messages."); - } + } } } @@ -422,47 +490,64 @@ public final class App extends Application { return str; } } - + + public boolean callNotificationsEnabled() + { + return tryGetBooleanSetting("call_notifications", false); + } + public String getServerUrl() { return settings.getString("server_url", ""); } - + public String getPhoneNumber() { return settings.getString("phone_number", ""); } - + public int getOutgoingPollSeconds() { return Integer.parseInt(settings.getString("outgoing_interval", "0")); } public boolean isEnabled() { - return settings.getBoolean("enabled", false); + return tryGetBooleanSetting("enabled", false); + } + + public boolean tryGetBooleanSetting(String name, boolean defaultValue) + { + try + { + return settings.getBoolean(name, defaultValue); + } + catch (Exception ex) + { + return defaultValue; + } } public boolean isNetworkFailoverEnabled() { - return settings.getBoolean("network_failover", false); + return tryGetBooleanSetting("network_failover", false); } public boolean isTestMode() { - return settings.getBoolean("test_mode", false); - } + return tryGetBooleanSetting("test_mode", false); + } public boolean getKeepInInbox() { - return settings.getBoolean("keep_in_inbox", false); + return tryGetBooleanSetting("keep_in_inbox", false); } public boolean ignoreShortcodes() { - return settings.getBoolean("ignore_shortcodes", true); + return tryGetBooleanSetting("ignore_shortcodes", true); } public boolean ignoreNonNumeric() { - return settings.getBoolean("ignore_non_numeric", true); + return tryGetBooleanSetting("ignore_non_numeric", true); } public String getPassword() { @@ -644,6 +729,11 @@ public final class App extends Application { ).commit(); } + public synchronized void saveStringSetting(String key, String value) + { + settings.edit().putString(key, value).commit(); + } + public synchronized void saveBooleanSetting(String key, boolean value) { settings.edit().putBoolean(key, value).commit(); @@ -761,21 +851,21 @@ public final class App extends Application { public synchronized boolean canCheck() { - long time = SystemClock.elapsedRealtime(); + long time = System.currentTimeMillis(); return (time - lastCheckTime >= App.CONNECTIVITY_FAILOVER_INTERVAL); } public void setChecked() { - lastCheckTime = SystemClock.elapsedRealtime(); + lastCheckTime = System.currentTimeMillis(); } } private Map connectivityCheckStates = new HashMap(); - - private Thread connectivityThread; - + + private CheckConnectivityTask checkConnectivityTask; + /* * Normally we rely on Android to automatically switch between * mobile data and Wi-Fi, but if the phone is connected to a Wi-Fi router @@ -820,7 +910,7 @@ public final class App extends Application { return; } - final int networkType = activeNetwork.getType(); + final int networkType = activeNetwork.getType(); ConnectivityCheckState state = connectivityCheckStates.get(networkType); @@ -832,79 +922,20 @@ public final class App extends Application { } if (!state.canCheck() - || (connectivityThread != null && connectivityThread.isAlive())) + || (checkConnectivityTask != null && checkConnectivityTask.getStatus() != AsyncTask.Status.FINISHED)) { return; } state.setChecked(); - connectivityThread = new Thread() { - @Override - public void run() - { - Uri serverUrl = Uri.parse(getServerUrl()); - String hostName = serverUrl.getHost(); + Uri serverUrl = Uri.parse(getServerUrl()); + String hostName = serverUrl.getHost(); - log("Checking connectivity to "+hostName+"..."); - - try - { - InetAddress addr = InetAddress.getByName(hostName); - if (addr.isReachable(App.HTTP_CONNECTION_TIMEOUT)) - { - log("OK"); - onConnectivityRestored(); - return; - } - } - catch (IOException ex) - { - // just what we suspected... - // server not reachable on this interface - } - - log("Can't connect to "+hostName+"."); - - WifiManager wmgr = (WifiManager)getSystemService(Context.WIFI_SERVICE); - - if (!isNetworkFailoverEnabled()) - { - log("Network failover disabled."); - } - else if (networkType == ConnectivityManager.TYPE_WIFI) - { - log("Switching from WIFI to MOBILE"); - - PendingIntent pendingIntent = PendingIntent.getBroadcast(App.this, - 0, - new Intent(App.this, ReenableWifiReceiver.class), - 0); - - // set an alarm to try restoring Wi-Fi in a little while - AlarmManager alarm = - (AlarmManager)getSystemService(Context.ALARM_SERVICE); - - alarm.set( - AlarmManager.ELAPSED_REALTIME_WAKEUP, - SystemClock.elapsedRealtime() + App.DISABLE_WIFI_INTERVAL, - pendingIntent); - - wmgr.setWifiEnabled(false); - } - else if (networkType == ConnectivityManager.TYPE_MOBILE - && !wmgr.isWifiEnabled()) - { - log("Switching from MOBILE to WIFI"); - wmgr.setWifiEnabled(true); - } - else - { - log("Can't automatically fix connectivity."); - } - } - }; - connectivityThread.start(); + log("Checking connectivity to "+hostName+"..."); + + checkConnectivityTask = new CheckConnectivityTask(this, hostName, networkType); + checkConnectivityTask.execute(); } private int activeNetworkType = -1; @@ -933,8 +964,21 @@ public final class App extends Application { asyncCheckConnectivity(); } - private void onConnectivityRestored() + public boolean hasConnectivityError() { + return connectivityError; + } + + public synchronized void onConnectivityError() + { + connectivityError = true; + asyncCheckConnectivity(); + } + + public synchronized void onConnectivityRestored() + { + connectivityError = false; + inbox.retryAll(); if (getOutgoingPollSeconds() > 0) @@ -963,5 +1007,5 @@ public final class App extends Application { public synchronized void addQueuedTask(HttpTask task) { queuedTasks.add(task); - } + } } diff --git a/src/org/envaya/sms/CallListener.java b/src/org/envaya/sms/CallListener.java new file mode 100644 index 0000000..a553940 --- /dev/null +++ b/src/org/envaya/sms/CallListener.java @@ -0,0 +1,31 @@ +package org.envaya.sms; + +import android.telephony.PhoneStateListener; +import android.telephony.TelephonyManager; + +public class CallListener extends PhoneStateListener { + + private App app; + + public CallListener(App app) + { + this.app = app; + } + + @Override + public void onCallStateChanged(int state,String incomingNumber) { + if (state == TelephonyManager.CALL_STATE_RINGING) + { + IncomingCall call = new IncomingCall(app, incomingNumber, System.currentTimeMillis()); + + if (call.isForwardable()) + { + app.inbox.forwardMessage(call); + } + else + { + app.log("Ignoring incoming call from " + call.getFrom()); + } + } + } +} diff --git a/src/org/envaya/sms/ForegroundService.java b/src/org/envaya/sms/ForegroundService.java index d495ae1..6ee3987 100755 --- a/src/org/envaya/sms/ForegroundService.java +++ b/src/org/envaya/sms/ForegroundService.java @@ -26,7 +26,7 @@ import android.util.Log; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; -import org.envaya.sms.ui.Main; +import org.envaya.sms.ui.LogView; /* * Service running in foreground to make sure App instance stays @@ -159,7 +159,7 @@ public class ForegroundService extends Service { System.currentTimeMillis()); PendingIntent contentIntent = PendingIntent.getActivity(this, 0, - new Intent(this, Main.class), 0); + new Intent(this, LogView.class), 0); notification.setLatestEventInfo(this, "EnvayaSMS running", diff --git a/src/org/envaya/sms/IncomingCall.java b/src/org/envaya/sms/IncomingCall.java new file mode 100644 index 0000000..a7e2eaf --- /dev/null +++ b/src/org/envaya/sms/IncomingCall.java @@ -0,0 +1,49 @@ +/* + * To change this template, choose Tools | Templates + * and open the template in the editor. + */ +package org.envaya.sms; + +import android.net.Uri; +import org.envaya.sms.task.ForwarderTask; + +public class IncomingCall extends IncomingMessage { + + private long id; + + private static long nextId = 1; + + public IncomingCall(App app, String from, long timestampMillis) + { + super(app, from, timestampMillis); + this.id = getNextId(); + } + + public static synchronized long getNextId() + { + long id = nextId; + nextId++; + return id; + } + + public String getDisplayType() + { + return "call"; + } + + public String getMessageType() + { + return App.MESSAGE_TYPE_CALL; + } + + @Override + public boolean isForwardable() + { + return app.callNotificationsEnabled() && super.isForwardable(); + } + + public Uri getUri() + { + return Uri.withAppendedPath(App.INCOMING_URI, "call/" + id); + } +} diff --git a/src/org/envaya/sms/IncomingMessage.java b/src/org/envaya/sms/IncomingMessage.java index 26c16a3..02fef46 100755 --- a/src/org/envaya/sms/IncomingMessage.java +++ b/src/org/envaya/sms/IncomingMessage.java @@ -1,13 +1,19 @@ package org.envaya.sms; import android.content.Intent; +import android.os.SystemClock; import org.envaya.sms.receiver.IncomingMessageRetry; +import org.envaya.sms.task.ForwarderTask; +import org.apache.http.message.BasicNameValuePair; public abstract class IncomingMessage extends QueuedMessage { protected String from; + protected String message = ""; protected long timestamp; // unix timestamp in milliseconds + protected long timeReceived; // SystemClock.elapsedRealtime + private ProcessingState state = ProcessingState.None; public enum ProcessingState @@ -24,6 +30,18 @@ public abstract class IncomingMessage extends QueuedMessage { super(app); this.from = from; this.timestamp = timestamp; + + this.timeReceived = SystemClock.elapsedRealtime(); + } + + public String getMessageBody() + { + return message; + } + + public long getAge() + { + return SystemClock.elapsedRealtime() - timeReceived; } public long getTimestamp() @@ -77,5 +95,27 @@ public abstract class IncomingMessage extends QueuedMessage { return getDisplayType() + " from " + getFrom(); } - public abstract void tryForwardToServer(); + public void tryForwardToServer() + { + if (numRetries > 0) + { + app.log("Retrying forwarding " + getDescription()); + } + + getForwarderTask().execute(); + } + + public abstract String getMessageType(); + + protected ForwarderTask getForwarderTask() + { + return new ForwarderTask(this, + new BasicNameValuePair("message_type", getMessageType()), + new BasicNameValuePair("message", getMessageBody()), + new BasicNameValuePair("action", App.ACTION_INCOMING), + new BasicNameValuePair("from", getFrom()), + new BasicNameValuePair("timestamp", "" + getTimestamp()), + new BasicNameValuePair("age", "" + getAge()) + ); + } } diff --git a/src/org/envaya/sms/IncomingMms.java b/src/org/envaya/sms/IncomingMms.java index f3f5f2c..ed9f1a0 100755 --- a/src/org/envaya/sms/IncomingMms.java +++ b/src/org/envaya/sms/IncomingMms.java @@ -11,7 +11,6 @@ import java.util.List; import org.apache.http.entity.mime.FormBodyPart; import org.apache.http.entity.mime.content.ByteArrayBody; import org.apache.http.entity.mime.content.ContentBody; -import org.apache.http.message.BasicNameValuePair; import org.envaya.sms.task.ForwarderTask; public class IncomingMms extends IncomingMessage { @@ -76,22 +75,13 @@ public class IncomingMms extends IncomingMessage { return builder.toString(); } - public void tryForwardToServer() + @Override + protected ForwarderTask getForwarderTask() { - if (numRetries > 0) - { - app.log("Retrying forwarding MMS from " + from); - } - else - { - app.log("Forwarding MMS to server..."); - } - List formParts = new ArrayList(); int i = 0; - - String message = ""; + JSONArray partsMetadata = new JSONArray(); for (MmsPart part : parts) @@ -99,12 +89,7 @@ public class IncomingMms extends IncomingMessage { String formFieldName = "part" + i; String text = part.getText(); String contentType = part.getContentType(); - String partName = part.getName(); - - if ("text/plain".equals(contentType)) - { - message = text; - } + String partName = part.getName(); ContentBody body; @@ -154,18 +139,33 @@ public class IncomingMms extends IncomingMessage { i++; } - ForwarderTask task = new ForwarderTask(this, - new BasicNameValuePair("message", message), - new BasicNameValuePair("message_type", App.MESSAGE_TYPE_MMS), - new BasicNameValuePair("mms_parts", partsMetadata.toString()) - ); - + ForwarderTask task = super.getForwarderTask(); + task.addParam("mms_parts", partsMetadata.toString()); task.setFormParts(formParts); - task.execute(); - } + return task; + } + + @Override + public String getMessageBody() + { + for (MmsPart part : parts) + { + if ("text/plain".equals(part.getContentType())) + { + return part.getText(); + } + } + + return ""; + } public Uri getUri() { return Uri.withAppendedPath(App.INCOMING_URI, "mms/" + id); } + + public String getMessageType() + { + return App.MESSAGE_TYPE_MMS; + } } diff --git a/src/org/envaya/sms/IncomingSms.java b/src/org/envaya/sms/IncomingSms.java index 29f39ea..7bbf291 100755 --- a/src/org/envaya/sms/IncomingSms.java +++ b/src/org/envaya/sms/IncomingSms.java @@ -5,14 +5,11 @@ import android.net.Uri; import android.telephony.SmsMessage; import java.security.InvalidParameterException; import java.util.List; -import org.apache.http.message.BasicNameValuePair; import org.envaya.sms.task.ForwarderTask; public class IncomingSms extends IncomingMessage { - protected String message; - // constructor for SMS retrieved from android.provider.Telephony.SMS_RECEIVED intent public IncomingSms(App app, List smsParts) throws InvalidParameterException { super(app, @@ -42,12 +39,7 @@ public class IncomingSms extends IncomingMessage { public IncomingSms(App app, String from, String message, long timestampMillis) { super(app, from, timestampMillis); this.message = message; - } - - public String getMessageBody() - { - return message; - } + } public String getDisplayType() { @@ -62,18 +54,9 @@ public class IncomingSms extends IncomingMessage { + timestamp + "/" + Uri.encode(message)); } - - public void tryForwardToServer() { - - if (numRetries > 0) - { - app.log("Retrying forwarding SMS from " + from); - } - - new ForwarderTask(this, - new BasicNameValuePair("message_type", App.MESSAGE_TYPE_SMS), - new BasicNameValuePair("message", getMessageBody()) - ).execute(); - } + public String getMessageType() + { + return App.MESSAGE_TYPE_SMS; + } } diff --git a/src/org/envaya/sms/MmsUtils.java b/src/org/envaya/sms/MmsUtils.java index 4025471..a64a5b7 100755 --- a/src/org/envaya/sms/MmsUtils.java +++ b/src/org/envaya/sms/MmsUtils.java @@ -4,11 +4,7 @@ package org.envaya.sms; import android.content.ContentResolver; import android.database.Cursor; import android.net.Uri; -import java.io.File; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Set; +import java.util.*; /* * Utilities for parsing IncomingMms from the MMS content provider tables, @@ -56,9 +52,25 @@ public class MmsUtils { long partId = cur.getLong(0); - MmsPart part = new MmsPart(app, partId); - part.setContentType(cur.getString(1)); - part.setName(cur.getString(2)); + String contentType = cur.getString(1); + + if (contentType == null) + { + continue; + } + + MmsPart part = new MmsPart(app, partId); + + part.setContentType(contentType); + + String name = cur.getString(2); + + if (name == null || name.length() == 0) + { + name = UUID.randomUUID().toString(); + } + + part.setName(name); part.setDataFile(cur.getString(5)); @@ -116,9 +128,17 @@ public class MmsUtils { long id = c.getLong(0); long date = c.getLong(2); - + + String from = getSenderNumber(id); + + if (from == null) + { + app.log("Ignoring MMS "+id+" for now because sender number is null"); + continue; + } + IncomingMms mms = new IncomingMms(app, - getSenderNumber(id), + from, date * 1000, // MMS timestamp is in seconds for some reason, // while everything else is in ms id); diff --git a/src/org/envaya/sms/Outbox.java b/src/org/envaya/sms/Outbox.java index eb9929f..4162482 100755 --- a/src/org/envaya/sms/Outbox.java +++ b/src/org/envaya/sms/Outbox.java @@ -6,8 +6,7 @@ import android.app.PendingIntent; import android.content.Context; import android.content.Intent; import android.net.Uri; -import android.telephony.SmsManager; -import java.util.ArrayList; +import android.os.SystemClock; import java.util.Collection; import java.util.Comparator; import java.util.HashMap; @@ -19,6 +18,7 @@ import java.util.Queue; import java.util.Set; import org.apache.http.message.BasicNameValuePair; import org.envaya.sms.receiver.DequeueOutgoingMessageReceiver; +import org.envaya.sms.receiver.OutgoingMessagePoller; import org.envaya.sms.task.HttpTask; public class Outbox { @@ -39,7 +39,7 @@ public class Outbox { // cache of next time we can send the first message in queue without // exceeding android sending limit private long nextValidOutgoingTime; - + // enqueue outgoing messages in descending order by priority, ascending by local id // (order in which message was received) private PriorityQueue outgoingQueue = new PriorityQueue(10, @@ -75,7 +75,10 @@ public class Outbox { logMessage = "sent successfully"; } else if (status.equals(App.STATUS_FAILED)) { logMessage = "could not be sent (" + errorMessage + ")"; - } else { + } else if (status.equals(App.STATUS_CANCELLED)) { + logMessage = "cancelled"; + } + else { logMessage = "queued"; } String smsDesc = sms.getLogName(); @@ -116,6 +119,8 @@ public class Outbox { { sms.setProcessingState(OutgoingMessage.ProcessingState.Sent); + sms.clearSendTimeout(); + notifyMessageStatus(sms, App.STATUS_SENT, ""); Uri uri = sms.getUri(); @@ -146,9 +151,11 @@ public class Outbox { } } } - + public synchronized void messageFailed(OutgoingMessage sms, String error) { + sms.clearSendTimeout(); + if (sms.scheduleRetry()) { sms.setProcessingState(OutgoingMessage.ProcessingState.Scheduled); @@ -164,49 +171,33 @@ public class Outbox { maybeDequeueMessage(); } - public synchronized void sendMessage(OutgoingMessage sms) { - - String to = sms.getTo(); - if (to == null || to.length() == 0) + public synchronized void sendMessage(OutgoingMessage message) { + + try { - notifyMessageStatus(sms, App.STATUS_FAILED, - "Destination address is empty"); - return; - } - - if (!app.isForwardablePhoneNumber(to)) + message.validate(); + } + catch (ValidationException ex) { - // this is mostly to prevent accidentally sending real messages to - // random people while testing... - notifyMessageStatus(sms, App.STATUS_FAILED, - "Destination address is not allowed"); - return; + notifyMessageStatus(message, App.STATUS_FAILED, ex.getMessage()); + return; } - String messageBody = sms.getMessageBody(); - - if (messageBody == null || messageBody.length() == 0) - { - notifyMessageStatus(sms, App.STATUS_FAILED, - "Message body is empty"); - return; - } - - Uri uri = sms.getUri(); + Uri uri = message.getUri(); if (outgoingMessages.containsKey(uri)) { - app.debug("Duplicate outgoing " + sms.getLogName() + ", skipping"); + app.debug("Duplicate outgoing " + message.getLogName() + ", skipping"); return; } if (recentSentMessageUris.contains(uri)) { - app.debug("Outgoing " + sms.getLogName() + " already sent, re-notifying server"); - notifyMessageStatus(sms, App.STATUS_SENT, ""); + app.debug("Outgoing " + message.getLogName() + " already sent, re-notifying server"); + notifyMessageStatus(message, App.STATUS_SENT, ""); return; } - outgoingMessages.put(uri, sms); - enqueueMessage(sms); + outgoingMessages.put(uri, message); + enqueueMessage(message); } public synchronized void deleteMessage(OutgoingMessage message) @@ -222,7 +213,7 @@ public class Outbox { numSendingOutgoingMessages--; } - notifyMessageStatus(message, App.STATUS_FAILED, + notifyMessageStatus(message, App.STATUS_CANCELLED, "deleted by user"); app.log(message.getDescription() + " deleted"); notifyChanged(); @@ -233,46 +224,32 @@ public class Outbox { long now = System.currentTimeMillis(); if (nextValidOutgoingTime <= now && numSendingOutgoingMessages < 2) { - OutgoingMessage sms = outgoingQueue.peek(); + OutgoingMessage message = outgoingQueue.peek(); - if (sms == null) + if (message == null) { return; } - SmsManager smgr = SmsManager.getDefault(); - ArrayList bodyParts = smgr.divideMessage(sms.getMessageBody()); + OutgoingMessage.ScheduleInfo schedule = message.scheduleSend(); - int numParts = bodyParts.size(); - - if (numParts > App.OUTGOING_SMS_MAX_COUNT) + if (!schedule.now) { - outgoingQueue.poll(); - outgoingMessages.remove(sms.getUri()); - notifyMessageStatus(sms, App.STATUS_FAILED, - "Message has too many parts ("+(numParts)+")"); - return; - } + nextValidOutgoingTime = schedule.time; - String packageName = app.chooseOutgoingSmsPackage(numParts); - - if (packageName == null) - { - nextValidOutgoingTime = app.getNextValidOutgoingTime(numParts); - if (nextValidOutgoingTime <= now) // should never happen { nextValidOutgoingTime = now + 2000; } - + long diff = nextValidOutgoingTime - now; - + app.log("Waiting for " + (diff/1000) + " seconds"); - + AlarmManager alarm = (AlarmManager) app.getSystemService(Context.ALARM_SERVICE); Intent intent = new Intent(app, DequeueOutgoingMessageReceiver.class); - + PendingIntent pendingIntent = PendingIntent.getBroadcast(app, 0, intent, @@ -282,16 +259,18 @@ public class Outbox { AlarmManager.RTC_WAKEUP, nextValidOutgoingTime, pendingIntent); - + return; } - + outgoingQueue.poll(); numSendingOutgoingMessages++; + + message.setProcessingState(OutgoingMessage.ProcessingState.Sending); + message.send(schedule); - sms.setProcessingState(OutgoingMessage.ProcessingState.Sending); + message.setSendTimeout(); - sms.trySend(bodyParts, packageName); notifyChanged(); } } diff --git a/src/org/envaya/sms/OutgoingMessage.java b/src/org/envaya/sms/OutgoingMessage.java index e6a89db..553e773 100755 --- a/src/org/envaya/sms/OutgoingMessage.java +++ b/src/org/envaya/sms/OutgoingMessage.java @@ -1,12 +1,16 @@ package org.envaya.sms; +import android.app.AlarmManager; +import android.app.PendingIntent; +import android.content.Context; import org.envaya.sms.receiver.OutgoingMessageRetry; import android.content.Intent; import android.net.Uri; -import java.util.ArrayList; +import android.os.SystemClock; +import org.envaya.sms.receiver.OutgoingMessageTimeout; -public class OutgoingMessage extends QueuedMessage { +public abstract class OutgoingMessage extends QueuedMessage { private String serverId; private String message; @@ -18,6 +22,12 @@ public class OutgoingMessage extends QueuedMessage { private ProcessingState state = ProcessingState.None; + public class ScheduleInfo + { + public boolean now = false; + public long time = 0; + } + public enum ProcessingState { None, // not doing anything with this sms now... just sitting around @@ -43,6 +53,13 @@ public class OutgoingMessage extends QueuedMessage { this.state = status; } + public boolean isCancelable() + { + return this.state == ProcessingState.None + || this.state == ProcessingState.Queued + || this.state == ProcessingState.Scheduled; + } + static synchronized int getNextLocalId() { return nextLocalId++; @@ -59,10 +76,10 @@ public class OutgoingMessage extends QueuedMessage { ("_o" + localId) : serverId)); } - public String getLogName() + public static Uri getUriForServerId(String serverId) { - return (serverId == null) ? "SMS reply" : ("SMS id=" + serverId); - } + return Uri.withAppendedPath(App.OUTGOING_URI, serverId); + } public String getServerId() { @@ -112,32 +129,7 @@ public class OutgoingMessage extends QueuedMessage { public int getPriority() { return priority; - } - - public void trySend(ArrayList bodyParts, String packageName) - { - if (numRetries == 0) - { - app.log("Sending " + getLogName() + " to " + getTo()); - } - else - { - app.log("Retrying sending " + getLogName() + " to " + getTo()); - } - - int numParts = bodyParts.size(); - if (numParts > 1) - { - app.log("(Multipart message with "+numParts+" parts)"); - } - - Intent intent = new Intent(packageName + App.OUTGOING_SMS_INTENT_SUFFIX, this.getUri()); - intent.putExtra(App.OUTGOING_SMS_EXTRA_DELIVERY_REPORT, false); - intent.putExtra(App.OUTGOING_SMS_EXTRA_TO, getTo()); - intent.putExtra(App.OUTGOING_SMS_EXTRA_BODY, bodyParts); - - app.sendBroadcast(intent, "android.permission.SEND_SMS"); - } + } protected Intent getRetryIntent() { Intent intent = new Intent(app, OutgoingMessageRetry.class); @@ -165,8 +157,42 @@ public class OutgoingMessage extends QueuedMessage { return getDisplayType() + " to " + getTo(); } - public String getDisplayType() + abstract String getLogName(); + + public void validate() throws ValidationException { - return "SMS"; + } + + abstract ScheduleInfo scheduleSend(); + abstract void send(ScheduleInfo schedule); + + protected PendingIntent getTimeoutPendingIntent() + { + Intent timeout = new Intent(app, OutgoingMessageTimeout.class); + timeout.setData(getUri()); + + return PendingIntent.getBroadcast(app, + 0, + timeout, + 0); + } + + public void setSendTimeout() + { + AlarmManager alarm = (AlarmManager) app.getSystemService(Context.ALARM_SERVICE); + + PendingIntent timeoutIntent = getTimeoutPendingIntent(); + + alarm.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, + SystemClock.elapsedRealtime() + App.MESSAGE_SEND_TIMEOUT, timeoutIntent); + } + + public void clearSendTimeout() + { + AlarmManager alarm = (AlarmManager) app.getSystemService(Context.ALARM_SERVICE); + + PendingIntent timeoutIntent = getTimeoutPendingIntent(); + + alarm.cancel(timeoutIntent); } } diff --git a/src/org/envaya/sms/OutgoingSms.java b/src/org/envaya/sms/OutgoingSms.java new file mode 100644 index 0000000..13a8840 --- /dev/null +++ b/src/org/envaya/sms/OutgoingSms.java @@ -0,0 +1,127 @@ +package org.envaya.sms; + +import android.content.Intent; +import android.telephony.SmsManager; +import java.util.ArrayList; + +public class OutgoingSms extends OutgoingMessage { + + public OutgoingSms(App app) + { + super(app); + } + + public String getLogName() + { + return "SMS"; + } + + private ArrayList _bodyParts; + + public ArrayList getBodyParts() + { + if (_bodyParts == null) + { + SmsManager smgr = SmsManager.getDefault(); + _bodyParts = smgr.divideMessage(getMessageBody()); + } + return _bodyParts; + } + + public int getNumParts() + { + return getBodyParts().size(); + } + + public class ScheduleInfo extends OutgoingMessage.ScheduleInfo + { + public String packageName; + } + + public OutgoingMessage.ScheduleInfo scheduleSend() + { + ScheduleInfo schedule = new ScheduleInfo(); + + int numParts = getNumParts(); + String packageName = app.chooseOutgoingSmsPackage(numParts); + + if (packageName == null) + { + schedule.time = app.getNextValidOutgoingTime(numParts); + schedule.now = false; + } + else + { + schedule.now = true; + schedule.packageName = packageName; + } + + return schedule; + } + + public void send(OutgoingMessage.ScheduleInfo _schedule) + { + ScheduleInfo schedule = (ScheduleInfo)_schedule; + + if (numRetries == 0) + { + app.log("Sending " + getDescription()); + } + else + { + app.log("Retrying sending " + getDescription()); + } + + ArrayList bodyParts = getBodyParts(); + int numParts = bodyParts.size(); + if (numParts > 1) + { + app.log("(Multipart message with "+numParts+" parts)"); + } + + Intent intent = new Intent(schedule.packageName + App.OUTGOING_SMS_INTENT_SUFFIX, this.getUri()); + intent.putExtra(App.OUTGOING_SMS_EXTRA_DELIVERY_REPORT, false); + intent.putExtra(App.OUTGOING_SMS_EXTRA_TO, getTo()); + intent.putExtra(App.OUTGOING_SMS_EXTRA_BODY, bodyParts); + + app.sendBroadcast(intent, "android.permission.SEND_SMS"); + } + + public String getDisplayType() + { + return "SMS"; + } + + @Override + public void validate() throws ValidationException + { + super.validate(); + + String to = getTo(); + if (to == null || to.length() == 0) + { + throw new ValidationException("Destination address is empty"); + } + + if (!app.isForwardablePhoneNumber(to)) + { + // this is mostly to prevent accidentally sending real messages to + // random people while testing... + throw new ValidationException("Destination address is not allowed"); + } + + String messageBody = getMessageBody(); + + if (messageBody == null || messageBody.length() == 0) + { + throw new ValidationException("Message body is empty"); + } + + int numParts = getNumParts(); + + if (numParts > App.OUTGOING_SMS_MAX_COUNT) + { + throw new ValidationException("Message has too many parts ("+numParts+")"); + } + } +} diff --git a/src/org/envaya/sms/ValidationException.java b/src/org/envaya/sms/ValidationException.java new file mode 100644 index 0000000..ac3b462 --- /dev/null +++ b/src/org/envaya/sms/ValidationException.java @@ -0,0 +1,9 @@ +package org.envaya.sms; + +public class ValidationException extends Exception { + + public ValidationException(String message) + { + super(message); + } +} diff --git a/src/org/envaya/sms/XmlUtils.java b/src/org/envaya/sms/XmlUtils.java new file mode 100644 index 0000000..f76668a --- /dev/null +++ b/src/org/envaya/sms/XmlUtils.java @@ -0,0 +1,41 @@ +package org.envaya.sms; + +import java.io.IOException; +import java.io.InputStream; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import org.apache.http.HttpResponse; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.NodeList; +import org.xml.sax.SAXException; + +public class XmlUtils { + + public static Document parseResponse(HttpResponse response) + throws IOException, ParserConfigurationException, SAXException { + InputStream responseStream = response.getEntity().getContent(); + DocumentBuilder xmlBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder(); + return xmlBuilder.parse(responseStream); + } + + public static String getElementText(Element element) { + StringBuilder text = new StringBuilder(); + NodeList childNodes = element.getChildNodes(); + int numChildren = childNodes.getLength(); + for (int j = 0; j < numChildren; j++) { + text.append(childNodes.item(j).getNodeValue()); + } + return text.toString(); + } + + public static String getErrorText(Document xml) { + NodeList errorNodes = xml.getElementsByTagName("error"); + if (errorNodes.getLength() > 0) { + Element errorElement = (Element) errorNodes.item(0); + return getElementText(errorElement); + } + return null; + } +} diff --git a/src/org/envaya/sms/receiver/ExpansionPackInstallReceiver.java b/src/org/envaya/sms/receiver/ExpansionPackInstallReceiver.java index 5ec2560..fdfc9f1 100755 --- a/src/org/envaya/sms/receiver/ExpansionPackInstallReceiver.java +++ b/src/org/envaya/sms/receiver/ExpansionPackInstallReceiver.java @@ -12,11 +12,16 @@ public class ExpansionPackInstallReceiver extends BroadcastReceiver { App app = (App) context.getApplicationContext(); + String action = intent.getAction(); + String packageName = intent.getData().getSchemeSpecificPart(); - if (packageName != null && packageName.startsWith(context.getPackageName() + ".pack")) + if (packageName != null) { - app.updateExpansionPacks(); + if (packageName.startsWith(context.getPackageName() + ".pack")) + { + app.updateExpansionPacks(); + } } } } \ No newline at end of file diff --git a/src/org/envaya/sms/receiver/MessageStatusNotifier.java b/src/org/envaya/sms/receiver/MessageStatusNotifier.java index c18964d..87e437e 100755 --- a/src/org/envaya/sms/receiver/MessageStatusNotifier.java +++ b/src/org/envaya/sms/receiver/MessageStatusNotifier.java @@ -35,8 +35,8 @@ public class MessageStatusNotifier extends BroadcastReceiver { { // TODO: process message status for parts other than the first one return; - } - + } + int resultCode = getResultCode(); /* diff --git a/src/org/envaya/sms/receiver/OutgoingMessagePoller.java b/src/org/envaya/sms/receiver/OutgoingMessagePoller.java index 6787c2a..fe229b7 100755 --- a/src/org/envaya/sms/receiver/OutgoingMessagePoller.java +++ b/src/org/envaya/sms/receiver/OutgoingMessagePoller.java @@ -1,15 +1,15 @@ -package org.envaya.sms.receiver; - -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import org.envaya.sms.App; - -public class OutgoingMessagePoller extends BroadcastReceiver { - - @Override - public void onReceive(Context context, Intent intent) { - App app = (App) context.getApplicationContext(); - app.checkOutgoingMessages(); - } -} +package org.envaya.sms.receiver; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import org.envaya.sms.App; + +public class OutgoingMessagePoller extends BroadcastReceiver { + + @Override + public void onReceive(Context context, Intent intent) { + App app = (App) context.getApplicationContext(); + app.checkOutgoingMessages(); + } +} diff --git a/src/org/envaya/sms/receiver/OutgoingMessageTimeout.java b/src/org/envaya/sms/receiver/OutgoingMessageTimeout.java new file mode 100644 index 0000000..5faa76b --- /dev/null +++ b/src/org/envaya/sms/receiver/OutgoingMessageTimeout.java @@ -0,0 +1,29 @@ + +package org.envaya.sms.receiver; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import org.envaya.sms.App; +import org.envaya.sms.OutgoingMessage; + +public class OutgoingMessageTimeout extends BroadcastReceiver +{ + @Override + public void onReceive(Context context, Intent intent) + { + App app = (App) context.getApplicationContext(); + if (!app.isEnabled()) + { + return; + } + + OutgoingMessage message = app.outbox.getMessage(intent.getData()); + if (message == null) + { + return; + } + + app.outbox.messageFailed(message, "Timeout while attempting to send message"); + } +} diff --git a/src/org/envaya/sms/task/BaseHttpTask.java b/src/org/envaya/sms/task/BaseHttpTask.java new file mode 100644 index 0000000..94f1413 --- /dev/null +++ b/src/org/envaya/sms/task/BaseHttpTask.java @@ -0,0 +1,200 @@ +package org.envaya.sms.task; + +import android.os.AsyncTask; +import android.os.Build; +import org.envaya.sms.App; +import java.io.IOException; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import org.apache.http.Header; +import org.apache.http.HttpResponse; +import org.apache.http.client.HttpClient; +import org.apache.http.client.entity.UrlEncodedFormEntity; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.entity.mime.FormBodyPart; +import org.apache.http.entity.mime.MultipartEntity; +import org.apache.http.entity.mime.content.StringBody; +import org.apache.http.message.BasicNameValuePair; + +public class BaseHttpTask extends AsyncTask { + + protected App app; + protected String url; + protected List params = new ArrayList(); + + private List formParts; + protected boolean useMultipartPost = false; + protected HttpPost post; + protected Throwable requestException; + + public BaseHttpTask(App app, String url, BasicNameValuePair... paramsArr) + { + this.url = url; + this.app = app; + params = new ArrayList(Arrays.asList(paramsArr)); + } + + public void addParam(String name, String value) + { + params.add(new BasicNameValuePair(name, value)); + } + + public void setFormParts(List formParts) + { + useMultipartPost = true; + this.formParts = formParts; + } + + protected HttpPost makeHttpPost() throws Exception + { + HttpPost httpPost = new HttpPost(url); + + httpPost.setHeader("User-Agent", "EnvayaSMS/" + app.getPackageInfo().versionName + " (Android; SDK "+Build.VERSION.SDK_INT + "; " + Build.MANUFACTURER + "; " + Build.MODEL+")"); + + if (useMultipartPost) + { + MultipartEntity entity = new MultipartEntity();//HttpMultipartMode.BROWSER_COMPATIBLE); + + Charset charset = Charset.forName("UTF-8"); + + for (BasicNameValuePair param : params) + { + entity.addPart(param.getName(), new StringBody(param.getValue(), charset)); + } + + for (FormBodyPart formPart : formParts) + { + entity.addPart(formPart); + } + httpPost.setEntity(entity); + } + else + { + httpPost.setEntity(new UrlEncodedFormEntity(params, "UTF-8")); + } + + return httpPost; + } + + protected HttpResponse doInBackground(String... ignored) + { + try + { + post = makeHttpPost(); + HttpClient client = app.getHttpClient(); + return client.execute(post); + } + catch (Throwable ex) + { + requestException = ex; + + try + { + String message = ex.getMessage(); + // workaround for https://issues.apache.org/jira/browse/HTTPCLIENT-881 + if ((ex instanceof IOException) + && message != null && message.equals("Connection already shutdown")) + { + //app.log("Retrying request"); + post = makeHttpPost(); + HttpClient client = app.getHttpClient(); + return client.execute(post); + } + } + catch (Throwable ex2) + { + requestException = ex2; + } + } + + return null; + } + + public boolean isValidContentType(String contentType) + { + return true; // contentType.startsWith("text/xml"); + } + + @Override + protected void onPostExecute(HttpResponse response) { + if (response != null) + { + try + { + int statusCode = response.getStatusLine().getStatusCode(); + Header contentTypeHeader = response.getFirstHeader("Content-Type"); + String contentType = (contentTypeHeader != null) ? contentTypeHeader.getValue() : ""; + + boolean validContentType = isValidContentType(contentType); + + if (statusCode == 200) + { + if (validContentType) + { + handleResponse(response); + } + else + { + throw new Exception("Invalid response type " + contentType); + } + } + else if (statusCode >= 400 && statusCode <= 499 && validContentType) + { + handleErrorResponse(response); + handleFailure(); + } + else + { + throw new Exception("HTTP " + statusCode); + } + } + catch (Throwable ex) + { + post.abort(); + handleResponseException(ex); + handleFailure(); + } + + try + { + response.getEntity().consumeContent(); + } + catch (IOException ex) + { + } + } + else + { + handleRequestException(requestException); + handleFailure(); + } + } + + protected void handleResponse(HttpResponse response) throws Exception + { + // if we get a valid server response after a connectivity error, then forward any pending messages + if (app.hasConnectivityError()) + { + app.onConnectivityRestored(); + } + } + + protected void handleErrorResponse(HttpResponse response) throws Exception + { + } + + protected void handleFailure() + { + } + + protected void handleRequestException(Throwable ex) + { + } + + protected void handleResponseException(Throwable ex) + { + } + +} \ No newline at end of file diff --git a/src/org/envaya/sms/task/CheckConnectivityTask.java b/src/org/envaya/sms/task/CheckConnectivityTask.java new file mode 100644 index 0000000..0733101 --- /dev/null +++ b/src/org/envaya/sms/task/CheckConnectivityTask.java @@ -0,0 +1,101 @@ +package org.envaya.sms.task; + +import android.app.AlarmManager; +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.net.ConnectivityManager; +import android.net.wifi.WifiManager; +import android.os.AsyncTask; +import android.os.SystemClock; +import org.envaya.sms.App; +import org.envaya.sms.receiver.ReenableWifiReceiver; +import java.io.IOException; +import java.net.InetAddress; + + +public class CheckConnectivityTask extends AsyncTask { + + protected App app; + protected String hostName; + protected int networkType; + + public CheckConnectivityTask(App app, String hostName, int networkType) + { + this.app = app; + this.hostName = hostName; + this.networkType = networkType; + } + + protected Boolean doInBackground(String... ignored) + { + try + { + InetAddress addr = InetAddress.getByName(hostName); + if (addr.isReachable(App.HTTP_CONNECTION_TIMEOUT)) + { + return true; + } + } + catch (IOException ex) + { + // just what we suspected... + // server not reachable on this interface + } + + return false; + } + + + @Override + protected void onPostExecute(Boolean reachable) + { + if (reachable.booleanValue()) + { + app.log("OK"); + app.onConnectivityRestored(); + } + else + { + app.log("Can't connect to "+hostName+"."); + + WifiManager wmgr = (WifiManager)app.getSystemService(Context.WIFI_SERVICE); + + if (!app.isNetworkFailoverEnabled()) + { + app.debug("Network failover disabled."); + } + else if (networkType == ConnectivityManager.TYPE_WIFI) + { + app.log("Switching from WIFI to MOBILE"); + + PendingIntent pendingIntent = PendingIntent.getBroadcast(app, + 0, + new Intent(app, ReenableWifiReceiver.class), + 0); + + // set an alarm to try restoring Wi-Fi in a little while + AlarmManager alarm = + (AlarmManager)app.getSystemService(Context.ALARM_SERVICE); + + alarm.set( + AlarmManager.ELAPSED_REALTIME_WAKEUP, + SystemClock.elapsedRealtime() + App.DISABLE_WIFI_INTERVAL, + pendingIntent); + + wmgr.setWifiEnabled(false); + } + else if (networkType == ConnectivityManager.TYPE_MOBILE + && !wmgr.isWifiEnabled()) + { + app.log("Switching from MOBILE to WIFI"); + wmgr.setWifiEnabled(true); + } + else + { + app.log("Can't automatically fix connectivity."); + } + } + } + +} \ No newline at end of file diff --git a/src/org/envaya/sms/task/ForwarderTask.java b/src/org/envaya/sms/task/ForwarderTask.java index 5c72b62..3e1d2b9 100755 --- a/src/org/envaya/sms/task/ForwarderTask.java +++ b/src/org/envaya/sms/task/ForwarderTask.java @@ -2,7 +2,6 @@ package org.envaya.sms.task; import org.apache.http.HttpResponse; import org.apache.http.message.BasicNameValuePair; -import org.envaya.sms.App; import org.envaya.sms.IncomingMessage; import org.envaya.sms.OutgoingMessage; @@ -12,11 +11,13 @@ public class ForwarderTask extends HttpTask { public ForwarderTask(IncomingMessage message, BasicNameValuePair... paramsArr) { super(message.app, paramsArr); - this.message = message; - - params.add(new BasicNameValuePair("action", App.ACTION_INCOMING)); - params.add(new BasicNameValuePair("from", message.getFrom())); - params.add(new BasicNameValuePair("timestamp", "" + message.getTimestamp())); + this.message = message; + } + + @Override + public boolean isValidContentType(String contentType) + { + return contentType.startsWith("text/xml"); } @Override @@ -31,10 +32,12 @@ public class ForwarderTask extends HttpTask { app.outbox.sendMessage(reply); } app.inbox.messageForwarded(message); + + super.handleResponse(response); } @Override - protected void handleFailure() { + protected void handleFailure() { app.inbox.messageFailed(message); } } diff --git a/src/org/envaya/sms/task/HttpTask.java b/src/org/envaya/sms/task/HttpTask.java index a8c2099..8e2e1c0 100755 --- a/src/org/envaya/sms/task/HttpTask.java +++ b/src/org/envaya/sms/task/HttpTask.java @@ -4,79 +4,57 @@ */ package org.envaya.sms.task; -import android.os.AsyncTask; +import org.envaya.sms.XmlUtils; +import org.envaya.sms.OutgoingSms; +import org.envaya.sms.OutgoingMessage; +import org.envaya.sms.App; +import org.envaya.sms.Base64Coder; +import android.content.SharedPreferences; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; +import android.preference.PreferenceManager; import java.io.IOException; -import java.io.InputStream; import java.io.UnsupportedEncodingException; import java.net.SocketTimeoutException; import java.net.UnknownHostException; -import java.nio.charset.Charset; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.List; -import javax.xml.parsers.DocumentBuilder; -import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import org.apache.http.HttpResponse; -import org.apache.http.client.HttpClient; -import org.apache.http.client.entity.UrlEncodedFormEntity; import org.apache.http.client.methods.HttpPost; -import org.apache.http.entity.mime.FormBodyPart; -import org.apache.http.entity.mime.MultipartEntity; -import org.apache.http.entity.mime.content.StringBody; import org.apache.http.message.BasicNameValuePair; -import org.envaya.sms.App; -import org.envaya.sms.Base64Coder; -import org.envaya.sms.OutgoingMessage; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.NodeList; import org.xml.sax.SAXException; -public class HttpTask extends AsyncTask { +public class HttpTask extends BaseHttpTask { - protected App app; - - protected String url; - protected List params = new ArrayList(); - - protected BasicNameValuePair[] paramsArr; - - private List formParts; - private boolean useMultipartPost = false; - - private HttpPost post; private String logEntries; private boolean retryOnConnectivityError; + + private BasicNameValuePair[] ctorParams; public HttpTask(App app, BasicNameValuePair... paramsArr) { - super(); - this.app = app; - this.paramsArr = paramsArr; - params = new ArrayList(Arrays.asList(paramsArr)); + super(app, app.getServerUrl(), paramsArr); + this.ctorParams = paramsArr; } public void setRetryOnConnectivityError(boolean retry) { - this.retryOnConnectivityError = retry; + this.retryOnConnectivityError = retry; // doesn't work with addParam! } protected HttpTask getCopy() { - return new HttpTask(app, paramsArr); - } - - public void setFormParts(List formParts) - { - useMultipartPost = true; - this.formParts = formParts; - } + return new HttpTask(app, ctorParams); // doesn't work with addParam! + } private String getSignature() throws NoSuchAlgorithmException, UnsupportedEncodingException @@ -110,6 +88,7 @@ public class HttpTask extends AsyncTask { return new String(Base64Coder.encode(digest)); } + @Override protected HttpResponse doInBackground(String... ignored) { url = app.getServerUrl(); @@ -122,181 +101,136 @@ public class HttpTask extends AsyncTask { params.add(new BasicNameValuePair("version", "" + app.getPackageInfo().versionCode)); params.add(new BasicNameValuePair("phone_number", app.getPhoneNumber())); - params.add(new BasicNameValuePair("log", logEntries)); - - post = new HttpPost(url); + params.add(new BasicNameValuePair("send_limit", "" + app.getOutgoingMessageLimit())); - try - { - if (useMultipartPost) - { - MultipartEntity entity = new MultipartEntity();//HttpMultipartMode.BROWSER_COMPATIBLE); - - Charset charset = Charset.forName("UTF-8"); - - for (BasicNameValuePair param : params) - { - entity.addPart(param.getName(), new StringBody(param.getValue(), charset)); - } - - for (FormBodyPart formPart : formParts) - { - entity.addPart(formPart); - } - post.setEntity(entity); - } - else - { - post.setEntity(new UrlEncodedFormEntity(params, "UTF-8")); - } - - HttpClient client = app.getHttpClient(); - - String signature = getSignature(); - - post.setHeader("X-Request-Signature", signature); - post.setHeader("User-Agent", "EnvayaSMS/" + app.getPackageInfo().versionName); - - HttpResponse response = client.execute(post); - - int statusCode = response.getStatusLine().getStatusCode(); - if (statusCode == 200) - { - return response; - } - else if (statusCode == 403) - { - response.getEntity().consumeContent(); - app.ungetNewLogEntries(logEntries); - app.log("Failed to authenticate to server"); - app.log("(Phone number or password may be incorrect)"); - return null; - } - else - { - response.getEntity().consumeContent(); - app.ungetNewLogEntries(logEntries); - app.log("Received HTTP " + statusCode + " from server"); - return null; - } - } - catch (IOException ex) + ConnectivityManager cm = + (ConnectivityManager)app.getSystemService(App.CONNECTIVITY_SERVICE); + + NetworkInfo activeNetwork = cm.getActiveNetworkInfo(); + if (activeNetwork != null) { - post.abort(); - app.ungetNewLogEntries(logEntries); - app.logError("Error while contacting server", ex); - - if (ex instanceof UnknownHostException || ex instanceof SocketTimeoutException) - { - if (retryOnConnectivityError) - { - app.addQueuedTask(getCopy()); - } - - app.asyncCheckConnectivity(); - } - return null; + params.add(new BasicNameValuePair("network", "" + activeNetwork.getTypeName())); } - catch (Throwable ex) - { - post.abort(); - app.ungetNewLogEntries(logEntries); - app.logError("Unexpected error while contacting server", ex, true); - return null; - } + + params.add(new BasicNameValuePair("log", logEntries)); + + return super.doInBackground(); + } + + @Override + protected HttpPost makeHttpPost() + throws Exception + { + HttpPost httpPost = super.makeHttpPost(); + + String signature = getSignature(); + + httpPost.setHeader("X-Request-Signature", signature); + + return httpPost; } protected String getDefaultToAddress() { return ""; } - + protected List parseResponseXML(HttpResponse response) throws IOException, ParserConfigurationException, SAXException { List messages = new ArrayList(); - InputStream responseStream = response.getEntity().getContent(); - DocumentBuilder xmlBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder(); - Document xml = xmlBuilder.parse(responseStream); + Document xml = XmlUtils.parseResponse(response); - NodeList smsNodes = xml.getElementsByTagName("sms"); - for (int i = 0; i < smsNodes.getLength(); i++) { - Element smsElement = (Element) smsNodes.item(i); - - OutgoingMessage sms = new OutgoingMessage(app); - - sms.setFrom(app.getPhoneNumber()); - - String to = smsElement.getAttribute("to"); - - sms.setTo(to.equals("") ? getDefaultToAddress() : to); - - String serverId = smsElement.getAttribute("id"); - - sms.setServerId(serverId.equals("") ? null : serverId); - - String priorityStr = smsElement.getAttribute("priority"); - - if (!priorityStr.equals("")) + Element messagesElement = (Element) xml.getElementsByTagName("messages").item(0); + if (messagesElement != null) + { + NodeList messageNodes = messagesElement.getChildNodes(); + int numNodes = messageNodes.getLength(); + for (int i = 0; i < numNodes; i++) { - try - { - sms.setPriority(Integer.parseInt(priorityStr)); - } - catch (NumberFormatException ex) - { - app.log("Invalid message priority: " + priorityStr); - } - } + Element messageElement = (Element) messageNodes.item(i); - StringBuilder messageBody = new StringBuilder(); - NodeList childNodes = smsElement.getChildNodes(); - int numChildren = childNodes.getLength(); - for (int j = 0; j < numChildren; j++) - { - messageBody.append(childNodes.item(j).getNodeValue()); + OutgoingMessage message = new OutgoingSms(app); + + message.setFrom(app.getPhoneNumber()); + + String to = messageElement.getAttribute("to"); + + message.setTo(to.equals("") ? getDefaultToAddress() : to); + + String serverId = messageElement.getAttribute("id"); + + message.setServerId(serverId.equals("") ? null : serverId); + + String priorityStr = messageElement.getAttribute("priority"); + + if (!priorityStr.equals("")) + { + try + { + message.setPriority(Integer.parseInt(priorityStr)); + } + catch (NumberFormatException ex) + { + app.log("Invalid message priority: " + priorityStr); + } + } + + message.setMessageBody(XmlUtils.getElementText(messageElement)); + + messages.add(message); } - - sms.setMessageBody(messageBody.toString()); - - messages.add(sms); } return messages; } @Override - protected void onPostExecute(HttpResponse response) { - if (response != null) + protected void handleFailure() + { + app.ungetNewLogEntries(logEntries); + } + + @Override + protected void handleResponseException(Throwable ex) + { + app.logError("Error in server response", ex); + } + + @Override + protected void handleRequestException(Throwable ex) + { + if (ex instanceof IOException) { - try - { - handleResponse(response); - } - catch (Throwable ex) - { - post.abort(); - app.logError("Error processing server response", ex); - handleFailure(); - } - try - { - response.getEntity().consumeContent(); - } - catch (IOException ex) - { + app.logError("Error while contacting server", ex); + + if (ex instanceof UnknownHostException || ex instanceof SocketTimeoutException) + { + if (retryOnConnectivityError) + { + app.addQueuedTask(getCopy()); + } + + app.onConnectivityError(); } } else { - handleFailure(); - } - } - - protected void handleResponse(HttpResponse response) throws Exception - { + app.logError("Unexpected error while contacting server", ex, true); + } } - protected void handleFailure() + @Override + public void handleErrorResponse(HttpResponse response) throws Exception { - } + Document xml = XmlUtils.parseResponse(response); + String error = XmlUtils.getErrorText(xml); + if (error != null) + { + app.log(error); + } + else + { + app.log("HTTP " +response.getStatusLine().getStatusCode()); + } + } } diff --git a/src/org/envaya/sms/task/PollerTask.java b/src/org/envaya/sms/task/PollerTask.java index 6f20923..561c2bd 100755 --- a/src/org/envaya/sms/task/PollerTask.java +++ b/src/org/envaya/sms/task/PollerTask.java @@ -12,6 +12,12 @@ public class PollerTask extends HttpTask { super(app, new BasicNameValuePair("action", App.ACTION_OUTGOING)); } + @Override + public boolean isValidContentType(String contentType) + { + return contentType.startsWith("text/xml"); + } + @Override protected void onPostExecute(HttpResponse response) { super.onPostExecute(response); diff --git a/src/org/envaya/sms/ui/Help.java b/src/org/envaya/sms/ui/Help.java index 8ad870b..3f25e41 100755 --- a/src/org/envaya/sms/ui/Help.java +++ b/src/org/envaya/sms/ui/Help.java @@ -1,14 +1,22 @@ package org.envaya.sms.ui; import android.app.Activity; +import android.app.AlertDialog; +import android.content.DialogInterface; +import android.content.DialogInterface.OnClickListener; +import android.content.Intent; import android.os.Bundle; +import android.preference.PreferenceManager; import android.text.Html; +import android.view.View; import android.widget.TextView; import org.envaya.sms.App; import org.envaya.sms.R; public class Help extends Activity { + private App app; + @Override public void onCreate(Bundle icicle) { super.onCreate(icicle); @@ -17,19 +25,44 @@ public class Help extends Activity { TextView help = (TextView) this.findViewById(R.id.help); - App app = (App)getApplication(); + app = (App)getApplication(); String html = "EnvayaSMS " + app.getPackageInfo().versionName + "

" - + "EnvayaSMS is a SMS gateway.

" - + "It forwards all incoming SMS messages received by this phone to a server on the internet, " - + "and also sends outgoing SMS messages from that server to other phones.

" - + "See sms.envaya.org for more information.

" - + "The Settings screen allows you configure EnvayaSMS to work with a particular server, " - + "by entering the server URL, your phone number, " - + "and the password assigned to your phone on the server.

" + "Menu icons cc/by www.androidicons.com

"; help.setText(Html.fromHtml(html)); } + + public void resetClicked(View v) + { + new AlertDialog.Builder(this) + .setTitle("Are you sure?") + .setPositiveButton("Yes", + new OnClickListener() { + public void onClick(DialogInterface dialog, int which) + { + PreferenceManager.getDefaultSharedPreferences(app) + .edit() + .clear() + .commit(); + + app.enabledChanged(); + + dialog.dismiss(); + + finish(); + } + } + ) + .setNegativeButton("Cancel", + new OnClickListener() { + public void onClick(DialogInterface dialog, int which) + { + dialog.dismiss(); + } + } + ) + .show(); + } } diff --git a/src/org/envaya/sms/ui/Main.java b/src/org/envaya/sms/ui/LogView.java old mode 100755 new mode 100644 similarity index 84% rename from src/org/envaya/sms/ui/Main.java rename to src/org/envaya/sms/ui/LogView.java index b4ddb5b..d26da51 --- a/src/org/envaya/sms/ui/Main.java +++ b/src/org/envaya/sms/ui/LogView.java @@ -1,157 +1,156 @@ -package org.envaya.sms.ui; - -import org.envaya.sms.task.HttpTask; -import android.app.Activity; -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.os.Bundle; -import android.preference.PreferenceManager; -import android.text.method.ScrollingMovementMethod; -import android.view.Menu; -import android.view.MenuInflater; -import android.view.MenuItem; -import android.view.View; -import android.widget.ScrollView; -import android.widget.TextView; -import org.apache.http.HttpResponse; -import org.apache.http.message.BasicNameValuePair; -import org.envaya.sms.App; -import org.envaya.sms.R; - -public class Main extends Activity { - - private App app; - - private BroadcastReceiver logReceiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - updateLogView(); - } - }; - - private ScrollView scrollView; - private TextView info; - - private class TestTask extends HttpTask - { - public TestTask() { - super(Main.this.app, new BasicNameValuePair("action", App.ACTION_TEST)); - } - - @Override - protected void handleResponse(HttpResponse response) throws Exception - { - parseResponseXML(response); - app.log("Server connection OK!"); - } - } - - private int lastLogEpoch = -1; - - public void updateLogView() - { - int logEpoch = app.getLogEpoch(); - CharSequence displayedLog = app.getDisplayedLog(); - - if (lastLogEpoch == logEpoch) - { - int beforeLen = info.getText().length(); - int afterLen = displayedLog.length(); - - if (beforeLen == afterLen) - { - return; - } - - info.append(displayedLog, beforeLen, afterLen); - } - else - { - info.setText(displayedLog); - lastLogEpoch = logEpoch; - } - - scrollView.post(new Runnable() { public void run() { - scrollView.fullScroll(View.FOCUS_DOWN); - } }); - } - - /** Called when the activity is first created. */ - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - app = (App) getApplication(); - - setContentView(R.layout.main); - PreferenceManager.setDefaultValues(this, R.xml.prefs, false); - - scrollView = (ScrollView) this.findViewById(R.id.info_scroll); - info = (TextView) this.findViewById(R.id.info); - - info.setMovementMethod(new ScrollingMovementMethod()); - - updateLogView(); - - IntentFilter logReceiverFilter = new IntentFilter(); - logReceiverFilter.addAction(App.LOG_CHANGED_INTENT); - registerReceiver(logReceiver, logReceiverFilter); - } - - @Override - public void onDestroy() - { - this.unregisterReceiver(logReceiver); - super.onDestroy(); - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - // Handle item selection - switch (item.getItemId()) { - case R.id.settings: - startActivity(new Intent(this, Prefs.class)); - return true; - case R.id.check_now: - app.checkOutgoingMessages(); - return true; - case R.id.retry_now: - app.retryStuckMessages(); - return true; - case R.id.forward_inbox: - startActivity(new Intent(this, MessagingInbox.class)); - return true; - case R.id.pending: - startActivity(new Intent(this, PendingMessages.class)); - return true; - case R.id.test: - app.log("Testing server connection..."); - new TestTask().execute(); - return true; - default: - return super.onOptionsItemSelected(item); - } - } - - // first time the Menu key is pressed - @Override - public boolean onCreateOptionsMenu(Menu menu) { - MenuInflater inflater = getMenuInflater(); - inflater.inflate(R.menu.mainmenu, menu); - - return(true); - } - - @Override - public boolean onPrepareOptionsMenu(Menu menu) { - MenuItem retryItem = menu.findItem(R.id.retry_now); - int pendingTasks = app.getPendingTaskCount(); - retryItem.setEnabled(pendingTasks > 0); - retryItem.setTitle("Retry All (" + pendingTasks + ")"); - - return true; - } - +package org.envaya.sms.ui; + +import org.envaya.sms.task.HttpTask; +import android.app.Activity; +import android.content.*; +import android.net.Uri; +import android.os.Bundle; +import android.preference.PreferenceManager; +import android.text.method.LinkMovementMethod; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.widget.ScrollView; +import android.widget.TextView; +import org.apache.http.HttpResponse; +import org.apache.http.message.BasicNameValuePair; +import org.envaya.sms.App; +import org.envaya.sms.R; + +public class LogView extends Activity { + + private App app; + + private BroadcastReceiver logReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + updateLogView(); + } + }; + + private ScrollView scrollView; + private TextView info; + + private class TestTask extends HttpTask + { + public TestTask() { + super(LogView.this.app, new BasicNameValuePair("action", App.ACTION_TEST)); + } + + @Override + protected void handleResponse(HttpResponse response) throws Exception + { + app.log("Server connection OK!"); + } + } + + private int lastLogEpoch = -1; + + public void updateLogView() + { + int logEpoch = app.getLogEpoch(); + CharSequence displayedLog = app.getDisplayedLog(); + + if (lastLogEpoch == logEpoch) + { + int beforeLen = info.getText().length(); + int afterLen = displayedLog.length(); + + if (beforeLen == afterLen) + { + return; + } + + info.append(displayedLog, beforeLen, afterLen); + } + else + { + info.setText(displayedLog); + lastLogEpoch = logEpoch; + } + + scrollView.post(new Runnable() { public void run() { + scrollView.fullScroll(View.FOCUS_DOWN); + } }); + } + + /** Called when the activity is first created. */ + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + app = (App) getApplication(); + + setContentView(R.layout.log_view); + PreferenceManager.setDefaultValues(this, R.xml.prefs, false); + + scrollView = (ScrollView) this.findViewById(R.id.info_scroll); + info = (TextView) this.findViewById(R.id.info); + + info.setMovementMethod(LinkMovementMethod.getInstance()); + + //info.setMovementMethod(new ScrollingMovementMethod()); + + updateLogView(); + + IntentFilter logReceiverFilter = new IntentFilter(); + logReceiverFilter.addAction(App.LOG_CHANGED_INTENT); + registerReceiver(logReceiver, logReceiverFilter); + } + + @Override + public void onDestroy() + { + this.unregisterReceiver(logReceiver); + super.onDestroy(); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + // Handle item selection + switch (item.getItemId()) { + case R.id.settings: + startActivity(new Intent(this, Prefs.class)); + return true; + case R.id.check_now: + app.checkOutgoingMessages(); + return true; + case R.id.retry_now: + app.retryStuckMessages(); + return true; + case R.id.forward_inbox: + startActivity(new Intent(this, MessagingInbox.class)); + return true; + case R.id.pending: + startActivity(new Intent(this, PendingMessages.class)); + return true; + case R.id.test: + app.log("Testing server connection..."); + new TestTask().execute(); + return true; + default: + return super.onOptionsItemSelected(item); + } + } + + // first time the Menu key is pressed + @Override + public boolean onCreateOptionsMenu(Menu menu) { + MenuInflater inflater = getMenuInflater(); + inflater.inflate(R.menu.mainmenu, menu); + + return(true); + } + + @Override + public boolean onPrepareOptionsMenu(Menu menu) { + MenuItem retryItem = menu.findItem(R.id.retry_now); + int pendingTasks = app.getPendingTaskCount(); + retryItem.setEnabled(pendingTasks > 0); + retryItem.setTitle("Retry All (" + pendingTasks + ")"); + + return true; + } + } \ No newline at end of file diff --git a/src/org/envaya/sms/ui/Prefs.java b/src/org/envaya/sms/ui/Prefs.java index f40ff65..7f9ab42 100755 --- a/src/org/envaya/sms/ui/Prefs.java +++ b/src/org/envaya/sms/ui/Prefs.java @@ -89,6 +89,10 @@ public class Prefs extends PreferenceActivity implements OnSharedPreferenceChang app.log("Server URL changed to: " + app.getDisplayString(app.getServerUrl())); } + else if (key.equals("call_notifications")) + { + app.log("Call notifications changed to: " + (app.callNotificationsEnabled() ? "ON": "OFF")); + } else if (key.equals("phone_number")) { app.log("Phone number changed to: " + app.getDisplayString(app.getPhoneNumber()));