5
0
mirror of https://github.com/cwinfo/envayasms.git synced 2024-11-09 10:20:25 +00:00

add PendingMessages activity for viewing/retrying/deleting pending messages; clean up UI for ForwardInbox; create Inbox and Outbox class to simplify App class

This commit is contained in:
Jesse Young 2011-09-30 23:03:06 -07:00
parent 1081f57580
commit d7f803e60e
36 changed files with 1097 additions and 680 deletions

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="12" android:versionCode="13"
android:versionName="2.0-rc2"> android:versionName="2.0">
<uses-sdk android:minSdkVersion="4" /> <uses-sdk android:minSdkVersion="4" />
@ -35,7 +35,10 @@
<activity android:name=".ui.TestPhoneNumbers" android:label="EnvayaSMS : Test Phone Numbers"> <activity android:name=".ui.TestPhoneNumbers" android:label="EnvayaSMS : Test Phone Numbers">
</activity> </activity>
<activity android:name=".ui.ForwardInbox" android:label="EnvayaSMS : Forward Inbox"> <activity android:name=".ui.MessagingInbox" android:label="EnvayaSMS : Forward Inbox">
</activity>
<activity android:name=".ui.PendingMessages" android:label="EnvayaSMS : Pending Messages">
</activity> </activity>
<activity android:name=".ui.Prefs" android:label="EnvayaSMS : Settings"> <activity android:name=".ui.Prefs" android:label="EnvayaSMS : Settings">

View File

@ -34,10 +34,6 @@ libs/httpmime-4.1.2.jar is (c) Apache Software Foundation
org.envaya.sms.Base64Coder is (c) 2003-2010 Christian d'Heureuse, org.envaya.sms.Base64Coder is (c) 2003-2010 Christian d'Heureuse,
released under MIT License (and others) released under MIT License (and others)
org.envaya.sms.ui.InertCheckBox and org.envaya.sms.ui.CheckableRelativeLayout
is (c) C<>dric Caron, released presumably into the public domain at
http://www.marvinlabs.com/2010/10/custom-listview-ability-check-items/
org.envaya.sms.App.chooseOutgoingSmsPackage and org.envaya.sms.App.chooseOutgoingSmsPackage and
org.envaya.sms.ForegroundService include code from Android, org.envaya.sms.ForegroundService include code from Android,
Copyright 2005-2009 The Android Open Source Project Copyright 2005-2009 The Android Open Source Project

View File

@ -11,7 +11,6 @@
android:layout_width="fill_parent" android:layout_width="fill_parent"
android:layout_height="fill_parent" android:layout_height="fill_parent"
android:id="@+id/help" android:id="@+id/help"
android:textSize="16sp"
android:autoLink="web" android:autoLink="web"
android:textColor="#FFFFFF" android:textColor="#FFFFFF"
android:layout_margin="5px"> android:layout_margin="5px">

View File

@ -9,10 +9,11 @@
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"
android:choiceMode="multipleChoice" /> />
<Button <TextView android:id="@android:id/empty"
android:layout_height="wrap_content" android:text="The inbox is empty."
android:layout_width="wrap_content" android:layout_width="fill_parent"
android:text="Forward selected" android:layout_height="fill_parent"
android:onClick="forwardSelected" /> android:layout_weight="1"
/>
</LinearLayout> </LinearLayout>

View File

@ -1,34 +1,23 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<org.envaya.sms.ui.CheckableRelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal" android:orientation="vertical"
android:layout_width="fill_parent" android:layout_width="fill_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="#cccccc"> android:background="#333333">
<org.envaya.sms.ui.InertCheckBox android:id="@+id/inbox_checkbox"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:focusable="false"
/>
<TextView <TextView
android:id="@+id/inbox_address" android:id="@+id/inbox_address"
android:layout_width="fill_parent" android:layout_width="fill_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_toRightOf="@id/inbox_checkbox" android:textColor="#FFFFFF"
android:layout_alignTop="@id/inbox_checkbox"
android:textColor="#333333"
android:layout_marginTop="4sp" android:layout_marginTop="4sp"
android:layout_marginLeft="6sp" android:layout_marginLeft="6sp"
android:focusable="false"
android:textSize="14sp"></TextView> android:textSize="14sp"></TextView>
<TextView <TextView
android:id="@+id/inbox_body" android:id="@+id/inbox_body"
android:layout_below="@id/inbox_address"
android:layout_alignLeft="@id/inbox_address"
android:layout_width="fill_parent" android:layout_width="fill_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:textColor="#666666" android:textColor="#CCCCCC"
android:layout_marginLeft="6sp"
android:layout_marginBottom="6sp" android:layout_marginBottom="6sp"
android:focusable="false"
android:paddingBottom="6sp"
android:textSize="14sp"></TextView> android:textSize="14sp"></TextView>
</org.envaya.sms.ui.CheckableRelativeLayout> </LinearLayout>

35
res/layout/pending_message.xml Executable file
View File

@ -0,0 +1,35 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:background="#333333">
<TextView
android:id="@+id/pending_address"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:textColor="#FFFFFF"
android:layout_marginTop="4sp"
android:layout_marginLeft="6sp"
android:focusable="false"
android:textSize="14sp"></TextView>
<TextView
android:id="@+id/pending_time"
android:layout_below="@id/pending_address"
android:layout_alignLeft="@id/pending_address"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="6sp"
android:textColor="#CCCCCC"
android:focusable="false"
android:textSize="14sp"></TextView>
<TextView
android:id="@+id/pending_status"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:textColor="#CCCCCC"
android:focusable="false"
android:paddingBottom="6sp"
android:layout_marginLeft="6sp"
android:textSize="14sp"></TextView>
</LinearLayout>

19
res/layout/pending_messages.xml Executable file
View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:paddingLeft="8dp"
android:paddingRight="8dp">
<ListView android:id="@android:id/list"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:layout_weight="1"
/>
<TextView android:id="@android:id/empty"
android:text="There are no pending messages."
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:layout_weight="1"
/>
</LinearLayout>

5
res/menu/inbox.xml Executable file
View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="@+id/forward_all"
android:title="Forward all" />
</menu>

View File

@ -15,8 +15,8 @@
<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" />
<item android:id="@+id/help" <item android:id="@+id/pending"
android:icon="@drawable/ic_menu_puzzle" android:icon="@drawable/ic_menu_dialog"
android:title="@string/help" /> android:title="@string/pending" />
</menu> </menu>

7
res/menu/pending_messages.xml Executable file
View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="@+id/retry_all"
android:title="Retry all" />
<item android:id="@+id/delete_all"
android:title="Delete all" />
</menu>

View File

@ -5,6 +5,7 @@
<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="retry_now">Retry</string> <string name="retry_now">Retry</string>
<string name="forward_inbox">Fwd Inbox...</string> <string name="forward_inbox">Fwd Inbox...</string>
<string name='service_started'>New SMS will be forwarded to server</string> <string name='service_started'>New SMS will be forwarded to server</string>

View File

@ -80,4 +80,14 @@
android:targetClass="org.envaya.sms.ui.TestPhoneNumbers" /> android:targetClass="org.envaya.sms.ui.TestPhoneNumbers" />
</PreferenceScreen> </PreferenceScreen>
<PreferenceScreen
android:key="help"
android:title="About EnvayaSMS"
>
<intent
android:action="android.intent.action.MAIN"
android:targetPackage="org.envaya.sms"
android:targetClass="org.envaya.sms.ui.Help" />
</PreferenceScreen>
</PreferenceScreen> </PreferenceScreen>

View File

@ -17,7 +17,6 @@ import android.net.wifi.WifiManager;
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.SmsManager;
import android.text.Html; import android.text.Html;
import android.text.SpannableStringBuilder; import android.text.SpannableStringBuilder;
import android.util.Log; import android.util.Log;
@ -25,12 +24,10 @@ import java.io.IOException;
import java.net.InetAddress; import java.net.InetAddress;
import java.text.DateFormat; import java.text.DateFormat;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Comparator;
import java.util.Date; import java.util.Date;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.PriorityQueue;
import org.apache.http.client.HttpClient; import org.apache.http.client.HttpClient;
import org.apache.http.conn.scheme.PlainSocketFactory; import org.apache.http.conn.scheme.PlainSocketFactory;
import org.apache.http.conn.scheme.Scheme; import org.apache.http.conn.scheme.Scheme;
@ -38,15 +35,12 @@ import org.apache.http.conn.scheme.SchemeRegistry;
import org.apache.http.conn.ssl.SSLSocketFactory; import org.apache.http.conn.ssl.SSLSocketFactory;
import org.apache.http.impl.client.DefaultHttpClient; import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager; import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.params.BasicHttpParams; import org.apache.http.params.BasicHttpParams;
import org.apache.http.params.HttpConnectionParams; import org.apache.http.params.HttpConnectionParams;
import org.apache.http.params.HttpParams; import org.apache.http.params.HttpParams;
import org.apache.http.params.HttpProtocolParams; import org.apache.http.params.HttpProtocolParams;
import org.envaya.sms.receiver.DequeueOutgoingMessageReceiver;
import org.envaya.sms.receiver.OutgoingMessagePoller; import org.envaya.sms.receiver.OutgoingMessagePoller;
import org.envaya.sms.receiver.ReenableWifiReceiver; import org.envaya.sms.receiver.ReenableWifiReceiver;
import org.envaya.sms.task.HttpTask;
import org.envaya.sms.task.PollerTask; import org.envaya.sms.task.PollerTask;
import org.json.JSONArray; import org.json.JSONArray;
import org.json.JSONException; import org.json.JSONException;
@ -68,7 +62,11 @@ public final class App extends Application {
public static final String LOG_NAME = "EnvayaSMS"; public static final String LOG_NAME = "EnvayaSMS";
// 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_INTENT = "org.envaya.sms.LOG"; public static final String LOG_CHANGED_INTENT = "org.envaya.sms.LOG_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 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_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";
@ -113,29 +111,8 @@ public final class App extends Application {
public static final int OUTGOING_SMS_CHECK_PERIOD = 3605000; // one hour plus 5 sec (in ms) public static final int OUTGOING_SMS_CHECK_PERIOD = 3605000; // one hour plus 5 sec (in ms)
public static final int OUTGOING_SMS_MAX_COUNT = 100; public static final int OUTGOING_SMS_MAX_COUNT = 100;
private Map<Uri, IncomingMessage> incomingMessages = new HashMap<Uri, IncomingMessage>(); public final Inbox inbox = new Inbox(this);
private Map<Uri, OutgoingMessage> outgoingMessages = new HashMap<Uri, OutgoingMessage>(); public final Outbox outbox = new Outbox(this);
private int numPendingOutgoingMessages = 0;
private PriorityQueue<OutgoingMessage> outgoingQueue = new PriorityQueue<OutgoingMessage>(10,
new Comparator<OutgoingMessage>() {
public int compare(OutgoingMessage t1, OutgoingMessage t2)
{
int pri2 = t2.getPriority();
int pri1 = t1.getPriority();
if (pri1 != pri2)
{
return pri2 - pri1;
}
int order2 = t2.getLocalId();
int order1 = t1.getLocalId();
return order1 - order2;
}
}
);
private SharedPreferences settings; private SharedPreferences settings;
private MmsObserver mmsObserver; private MmsObserver mmsObserver;
@ -148,15 +125,13 @@ public final class App extends Application {
// for this package and all expansion packs // for this package and all expansion packs
private List<String> outgoingMessagePackages = new ArrayList<String>(); private List<String> outgoingMessagePackages = new ArrayList<String>();
// count to provide round-robin selection of expansion packs
private int outgoingMessageCount = -1;
private long nextValidOutgoingTime;
// map of package name => sorted list of timestamps of outgoing messages // map of package name => sorted list of timestamps of outgoing messages
private HashMap<String, ArrayList<Long>> outgoingTimestamps private HashMap<String, ArrayList<Long>> outgoingTimestamps
= new HashMap<String, ArrayList<Long>>(); = new HashMap<String, ArrayList<Long>>();
// count to provide round-robin selection of expansion packs
private int outgoingMessageCount = -1;
private MmsUtils mmsUtils; private MmsUtils mmsUtils;
@Override @Override
@ -226,7 +201,7 @@ public final class App extends Application {
return packageInfo; return packageInfo;
} }
private synchronized String chooseOutgoingSmsPackage(int numParts) public synchronized String chooseOutgoingSmsPackage(int numParts)
{ {
outgoingMessageCount++; outgoingMessageCount++;
@ -280,7 +255,7 @@ public final class App extends Application {
* outgoing SMS with numParts parts. Only valid immediately after * outgoing SMS with numParts parts. Only valid immediately after
* chooseOutgoingSmsPackage returns null. * chooseOutgoingSmsPackage returns null.
*/ */
private synchronized long getNextValidOutgoingTime(int numParts) public synchronized long getNextValidOutgoingTime(int numParts)
{ {
long minTime = System.currentTimeMillis() + OUTGOING_SMS_CHECK_PERIOD; long minTime = System.currentTimeMillis() + OUTGOING_SMS_CHECK_PERIOD;
@ -449,300 +424,13 @@ public final class App extends Application {
return settings.getString("password", ""); return settings.getString("password", "");
} }
private void notifyStatus(OutgoingMessage sms, String status, String errorMessage) {
String serverId = sms.getServerId();
String logMessage;
if (status.equals(App.STATUS_SENT)) {
logMessage = "sent successfully";
} else if (status.equals(App.STATUS_FAILED)) {
logMessage = "could not be sent (" + errorMessage + ")";
} else {
logMessage = "queued";
}
String smsDesc = sms.getLogName();
if (serverId != null) {
log("Notifying server " + smsDesc + " " + logMessage);
new HttpTask(this,
new BasicNameValuePair("id", serverId),
new BasicNameValuePair("status", status),
new BasicNameValuePair("error", errorMessage),
new BasicNameValuePair("action", App.ACTION_SEND_STATUS)
).execute();
} else {
log(smsDesc + " " + logMessage);
}
}
public synchronized void retryStuckMessages() { public synchronized void retryStuckMessages() {
outbox.retryAll();
this.nextValidOutgoingTime = 0; inbox.retryAll();
retryStuckOutgoingMessages();
retryStuckIncomingMessages();
} }
public synchronized int getStuckMessageCount() { public synchronized int getPendingMessageCount() {
return outgoingMessages.size() + incomingMessages.size(); return outbox.size() + inbox.size();
}
public synchronized void retryStuckOutgoingMessages() {
for (OutgoingMessage sms : outgoingMessages.values()) {
OutgoingMessage.ProcessingState state = sms.getProcessingState();
if (state != OutgoingMessage.ProcessingState.Queued
&& state != OutgoingMessage.ProcessingState.Sending)
{
enqueueOutgoingMessage(sms);
}
}
maybeDequeueOutgoingMessage();
}
public synchronized void retryStuckIncomingMessages() {
for (IncomingMessage sms : incomingMessages.values()) {
IncomingMessage.ProcessingState state = sms.getProcessingState();
if (state != IncomingMessage.ProcessingState.Forwarding)
{
enqueueIncomingMessage(sms);
}
}
}
public synchronized void setIncomingMessageStatus(IncomingMessage message, boolean success) {
message.setProcessingState(IncomingMessage.ProcessingState.None);
Uri uri = message.getUri();
if (success)
{
incomingMessages.remove(uri);
if (message instanceof IncomingMms)
{
IncomingMms mms = (IncomingMms)message;
if (!getKeepInInbox())
{
log("Deleting MMS " + mms.getId() + " from inbox...");
mmsUtils.deleteFromInbox(mms);
}
}
}
else
{
if (message.scheduleRetry())
{
message.setProcessingState(IncomingMessage.ProcessingState.Scheduled);
}
else
{
incomingMessages.remove(uri);
}
}
}
public synchronized void notifyOutgoingMessageStatus(Uri uri, int resultCode, int partIndex, int numParts) {
OutgoingMessage sms = outgoingMessages.get(uri);
if (sms == null) {
return;
}
if (partIndex != 0)
{
// TODO: process message status for parts other than the first one
return;
}
switch (resultCode) {
case Activity.RESULT_OK:
this.notifyStatus(sms, App.STATUS_SENT, "");
break;
case SmsManager.RESULT_ERROR_GENERIC_FAILURE:
this.notifyStatus(sms, App.STATUS_FAILED, "generic failure");
break;
case SmsManager.RESULT_ERROR_RADIO_OFF:
this.notifyStatus(sms, App.STATUS_FAILED, "radio off");
break;
case SmsManager.RESULT_ERROR_NO_SERVICE:
this.notifyStatus(sms, App.STATUS_FAILED, "no service");
break;
case SmsManager.RESULT_ERROR_NULL_PDU:
this.notifyStatus(sms, App.STATUS_FAILED, "null PDU");
break;
default:
this.notifyStatus(sms, App.STATUS_FAILED, "unknown error");
break;
}
sms.setProcessingState(OutgoingMessage.ProcessingState.None);
switch (resultCode) {
case SmsManager.RESULT_ERROR_GENERIC_FAILURE:
case SmsManager.RESULT_ERROR_RADIO_OFF:
case SmsManager.RESULT_ERROR_NO_SERVICE:
if (sms.scheduleRetry()) {
sms.setProcessingState(OutgoingMessage.ProcessingState.Scheduled);
}
else {
outgoingMessages.remove(uri);
}
break;
default:
outgoingMessages.remove(uri);
break;
}
numPendingOutgoingMessages--;
maybeDequeueOutgoingMessage();
}
public synchronized void sendOutgoingMessage(OutgoingMessage sms) {
String to = sms.getTo();
if (to == null || to.length() == 0)
{
notifyStatus(sms, App.STATUS_FAILED, "Destination address is empty");
return;
}
if (isTestMode() && !isTestPhoneNumber(to))
{
// this is mostly to prevent accidentally sending real messages to
// random people while testing...
notifyStatus(sms, App.STATUS_FAILED, "Destination number is not in list of test senders");
return;
}
String messageBody = sms.getMessageBody();
if (messageBody == null || messageBody.length() == 0)
{
notifyStatus(sms, App.STATUS_FAILED, "Message body is empty");
return;
}
Uri uri = sms.getUri();
if (outgoingMessages.containsKey(uri)) {
debug("Duplicate outgoing " + sms.getLogName() + ", skipping");
return;
}
outgoingMessages.put(uri, sms);
enqueueOutgoingMessage(sms);
}
public synchronized void maybeDequeueOutgoingMessage()
{
long now = System.currentTimeMillis();
if (nextValidOutgoingTime <= now && numPendingOutgoingMessages < 2)
{
OutgoingMessage sms = outgoingQueue.peek();
if (sms == null)
{
return;
}
SmsManager smgr = SmsManager.getDefault();
ArrayList<String> bodyParts = smgr.divideMessage(sms.getMessageBody());
int numParts = bodyParts.size();
if (numParts > App.OUTGOING_SMS_MAX_COUNT)
{
outgoingQueue.poll();
outgoingMessages.remove(sms.getUri());
notifyStatus(sms, App.STATUS_FAILED, "Message has too many parts ("+(numParts)+")");
return;
}
String packageName = chooseOutgoingSmsPackage(numParts);
if (packageName == null)
{
nextValidOutgoingTime = getNextValidOutgoingTime(numParts);
if (nextValidOutgoingTime <= now) // should never happen
{
nextValidOutgoingTime = now + 2000;
}
long diff = nextValidOutgoingTime - now;
log("Waiting for " + (diff/1000) + " seconds");
AlarmManager alarm = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
Intent intent = new Intent(this, DequeueOutgoingMessageReceiver.class);
PendingIntent pendingIntent = PendingIntent.getBroadcast(this,
0,
intent,
0);
alarm.set(
AlarmManager.RTC_WAKEUP,
nextValidOutgoingTime,
pendingIntent);
return;
}
outgoingQueue.poll();
numPendingOutgoingMessages++;
sms.setProcessingState(OutgoingMessage.ProcessingState.Sending);
sms.trySend(bodyParts, packageName);
}
}
public synchronized void enqueueOutgoingMessage(OutgoingMessage sms)
{
outgoingQueue.add(sms);
sms.setProcessingState(OutgoingMessage.ProcessingState.Queued);
maybeDequeueOutgoingMessage();
}
public synchronized void forwardToServer(IncomingMessage message) {
Uri uri = message.getUri();
if (incomingMessages.containsKey(uri)) {
log("Duplicate incoming "+message.getDisplayType()+", skipping");
return;
}
incomingMessages.put(uri, message);
log("Received "+message.getDisplayType()+" from " + message.getFrom());
enqueueIncomingMessage(message);
}
public synchronized void enqueueIncomingMessage(IncomingMessage message)
{
message.setProcessingState(IncomingMessage.ProcessingState.Forwarding);
message.tryForwardToServer();
}
public synchronized void retryIncomingMessage(Uri uri) {
IncomingMessage message = incomingMessages.get(uri);
if (message != null
&& message.getProcessingState() == IncomingMessage.ProcessingState.Scheduled) {
enqueueIncomingMessage(message);
}
}
public synchronized void retryOutgoingMessage(Uri uri) {
OutgoingMessage sms = outgoingMessages.get(uri);
if (sms != null
&& sms.getProcessingState() == OutgoingMessage.ProcessingState.Scheduled) {
enqueueOutgoingMessage(sms);
}
} }
public void debug(String msg) { public void debug(String msg) {
@ -783,8 +471,7 @@ public final class App extends Application {
displayedLog.append(msg); displayedLog.append(msg);
displayedLog.append("\n"); displayedLog.append("\n");
Intent broadcast = new Intent(App.LOG_INTENT); sendBroadcast(new Intent(App.LOG_CHANGED_INTENT));
sendBroadcast(broadcast);
} }
public synchronized CharSequence getDisplayedLog() public synchronized CharSequence getDisplayedLog()
@ -1092,15 +779,14 @@ public final class App extends Application {
asyncCheckConnectivity(); asyncCheckConnectivity();
} }
public void onConnectivityRestored() private void onConnectivityRestored()
{ {
retryStuckIncomingMessages(); inbox.retryAll();
if (getOutgoingPollSeconds() > 0) if (getOutgoingPollSeconds() > 0)
{ {
checkOutgoingMessages(); checkOutgoingMessages();
} }
// failed outgoing message status notifications are dropped... // failed outgoing message status notifications are dropped...
} }
} }

View File

@ -45,7 +45,7 @@ public class CheckMmsInboxService extends IntentService
if (mms.isForwardable()) if (mms.isForwardable())
{ {
app.forwardToServer(mms); app.inbox.forwardMessage(mms);
} }
else else
{ {

View File

@ -26,13 +26,12 @@ import android.util.Log;
import java.lang.reflect.InvocationTargetException; import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import org.envaya.sms.R;
import org.envaya.sms.ui.Main; 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
* in memory (otherwise we could lose timestamps of sent messages * in memory (to avoid losing pending messages and timestamps of
* which could cause us to exceed Android's SMS sending limit) * sent messages).
* *
* Also adds notification to status bar. * Also adds notification to status bar.
*/ */

112
src/org/envaya/sms/Inbox.java Executable file
View File

@ -0,0 +1,112 @@
package org.envaya.sms;
import android.content.Intent;
import android.net.Uri;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
public class Inbox {
private Map<Uri, IncomingMessage> incomingMessages = new HashMap<Uri, IncomingMessage>();
private App app;
public Inbox(App app)
{
this.app = app;
}
public IncomingMessage getMessage(Uri uri)
{
return incomingMessages.get(uri);
}
public synchronized void forwardMessage(IncomingMessage message) {
Uri uri = message.getUri();
if (incomingMessages.containsKey(uri)) {
app.log("Duplicate incoming "+message.getDisplayType()+", skipping");
return;
}
incomingMessages.put(uri, message);
app.log("Received "+message.getDisplayType()+" from " + message.getFrom());
message.setProcessingState(IncomingMessage.ProcessingState.Forwarding);
message.tryForwardToServer();
notifyChanged();
}
public synchronized void retryForwardMessage(IncomingMessage message)
{
IncomingMessage.ProcessingState state = message.getProcessingState();
if (state == IncomingMessage.ProcessingState.Scheduled
|| state == IncomingMessage.ProcessingState.None)
{
message.setProcessingState(IncomingMessage.ProcessingState.Forwarding);
message.tryForwardToServer();
notifyChanged();
}
}
public synchronized void deleteMessage(IncomingMessage message)
{
incomingMessages.remove(message.getUri());
app.log("SMS from " + message.getFrom() + " deleted");
notifyChanged();
}
public synchronized void messageFailed(IncomingMessage message)
{
message.setProcessingState(IncomingMessage.ProcessingState.None);
if (message.scheduleRetry())
{
message.setProcessingState(IncomingMessage.ProcessingState.Scheduled);
}
notifyChanged();
}
public synchronized void messageForwarded(IncomingMessage message) {
message.setProcessingState(IncomingMessage.ProcessingState.Forwarded);
Uri uri = message.getUri();
incomingMessages.remove(uri);
notifyChanged();
if (message instanceof IncomingMms)
{
IncomingMms mms = (IncomingMms)message;
if (!app.getKeepInInbox())
{
app.log("Deleting MMS " + mms.getId() + " from inbox...");
app.getMmsUtils().deleteFromInbox(mms);
}
}
}
private void notifyChanged()
{
app.sendBroadcast(new Intent(App.INBOX_CHANGED_INTENT));
}
public synchronized void retryAll() {
for (IncomingMessage message : incomingMessages.values()) {
retryForwardMessage(message);
}
}
public synchronized int size() {
return incomingMessages.size();
}
public synchronized Collection<IncomingMessage> getMessages()
{
return incomingMessages.values();
}
}

View File

@ -14,7 +14,8 @@ public abstract class IncomingMessage extends QueuedMessage {
{ {
None, // not doing anything with this sms now... just sitting around None, // not doing anything with this sms now... just sitting around
Forwarding, // currently sending to server Forwarding, // currently sending to server
Scheduled // waiting for a while before retrying after failure forwarding Scheduled, // waiting for a while before retrying after failure forwarding
Forwarded
} }
public IncomingMessage(App app, String from, long timestamp) public IncomingMessage(App app, String from, long timestamp)
@ -39,9 +40,6 @@ public abstract class IncomingMessage extends QueuedMessage {
this.state = status; this.state = status;
} }
public abstract String getDisplayType();
public boolean isForwardable() public boolean isForwardable()
{ {
if (app.isTestMode() && !app.isTestPhoneNumber(from)) if (app.isTestMode() && !app.isTestPhoneNumber(from))
@ -86,5 +84,23 @@ public abstract class IncomingMessage extends QueuedMessage {
return intent; return intent;
} }
public String getStatusText()
{
switch (state)
{
case Scheduled:
return "scheduled retry";
case Forwarding:
return "forwarding to server";
default:
return "";
}
}
public String getDescription()
{
return getDisplayType() + " from " + getFrom();
}
public abstract void tryForwardToServer(); public abstract void tryForwardToServer();
} }

280
src/org/envaya/sms/Outbox.java Executable file
View File

@ -0,0 +1,280 @@
package org.envaya.sms;
import android.app.AlarmManager;
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 java.util.Collection;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Map;
import java.util.PriorityQueue;
import org.apache.http.message.BasicNameValuePair;
import org.envaya.sms.receiver.DequeueOutgoingMessageReceiver;
import org.envaya.sms.task.HttpTask;
public class Outbox {
private Map<Uri, OutgoingMessage> outgoingMessages = new HashMap<Uri, OutgoingMessage>();
private App app;
// number of outgoing messages that are currently being sent and waiting for
// messageSent or messageFailed to be called
private int numSendingOutgoingMessages = 0;
// cache of next time we can send the first message in queue without
// exceeding android sending limit
private long nextValidOutgoingTime;
// enqueue outgoing messages in descending order by priority, ascending by local id
// (order in which message was received)
private PriorityQueue<OutgoingMessage> outgoingQueue = new PriorityQueue<OutgoingMessage>(10,
new Comparator<OutgoingMessage>() {
public int compare(OutgoingMessage t1, OutgoingMessage t2)
{
int pri2 = t2.getPriority();
int pri1 = t1.getPriority();
if (pri1 != pri2)
{
return pri2 - pri1;
}
int order2 = t2.getLocalId();
int order1 = t1.getLocalId();
return order1 - order2;
}
}
);
public Outbox(App app)
{
this.app = app;
}
private void notifyMessageStatus(OutgoingMessage sms, String status, String errorMessage) {
String serverId = sms.getServerId();
String logMessage;
if (status.equals(App.STATUS_SENT)) {
logMessage = "sent successfully";
} else if (status.equals(App.STATUS_FAILED)) {
logMessage = "could not be sent (" + errorMessage + ")";
} else {
logMessage = "queued";
}
String smsDesc = sms.getLogName();
if (serverId != null) {
app.log("Notifying server " + smsDesc + " " + logMessage);
new HttpTask(app,
new BasicNameValuePair("id", serverId),
new BasicNameValuePair("status", status),
new BasicNameValuePair("error", errorMessage),
new BasicNameValuePair("action", App.ACTION_SEND_STATUS)
).execute();
} else {
app.log(smsDesc + " " + logMessage);
}
}
public synchronized void retryAll()
{
nextValidOutgoingTime = 0;
for (OutgoingMessage sms : outgoingMessages.values()) {
enqueueMessage(sms);
}
maybeDequeueMessage();
}
public OutgoingMessage getMessage(Uri uri)
{
return outgoingMessages.get(uri);
}
public synchronized void messageSent(OutgoingMessage sms)
{
sms.setProcessingState(OutgoingMessage.ProcessingState.Sent);
notifyMessageStatus(sms, App.STATUS_SENT, "");
outgoingMessages.remove(sms.getUri());
notifyChanged();
numSendingOutgoingMessages--;
maybeDequeueMessage();
}
public synchronized void messageFailed(OutgoingMessage sms, String error)
{
if (sms.scheduleRetry())
{
sms.setProcessingState(OutgoingMessage.ProcessingState.Scheduled);
}
else
{
sms.setProcessingState(OutgoingMessage.ProcessingState.None);
}
notifyChanged();
notifyMessageStatus(sms, App.STATUS_FAILED, error);
numSendingOutgoingMessages--;
maybeDequeueMessage();
}
public synchronized void sendMessage(OutgoingMessage sms) {
String to = sms.getTo();
if (to == null || to.length() == 0)
{
notifyMessageStatus(sms, App.STATUS_FAILED,
"Destination address is empty");
return;
}
if (app.isTestMode() && !app.isTestPhoneNumber(to))
{
// this is mostly to prevent accidentally sending real messages to
// random people while testing...
notifyMessageStatus(sms, App.STATUS_FAILED,
"Destination number is not in list of test senders");
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();
if (outgoingMessages.containsKey(uri)) {
app.debug("Duplicate outgoing " + sms.getLogName() + ", skipping");
return;
}
outgoingMessages.put(uri, sms);
enqueueMessage(sms);
}
public synchronized void deleteMessage(OutgoingMessage message)
{
outgoingMessages.remove(message.getUri());
if (message.getProcessingState() == OutgoingMessage.ProcessingState.Queued)
{
outgoingQueue.remove(message);
}
notifyMessageStatus(message, App.STATUS_FAILED,
"deleted by user");
app.log("SMS to " + message.getTo() + " deleted");
notifyChanged();
}
public synchronized void maybeDequeueMessage()
{
long now = System.currentTimeMillis();
if (nextValidOutgoingTime <= now && numSendingOutgoingMessages < 2)
{
OutgoingMessage sms = outgoingQueue.peek();
if (sms == null)
{
return;
}
SmsManager smgr = SmsManager.getDefault();
ArrayList<String> bodyParts = smgr.divideMessage(sms.getMessageBody());
int numParts = bodyParts.size();
if (numParts > App.OUTGOING_SMS_MAX_COUNT)
{
outgoingQueue.poll();
outgoingMessages.remove(sms.getUri());
notifyMessageStatus(sms, App.STATUS_FAILED,
"Message has too many parts ("+(numParts)+")");
return;
}
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,
0);
alarm.set(
AlarmManager.RTC_WAKEUP,
nextValidOutgoingTime,
pendingIntent);
return;
}
outgoingQueue.poll();
numSendingOutgoingMessages++;
sms.setProcessingState(OutgoingMessage.ProcessingState.Sending);
sms.trySend(bodyParts, packageName);
notifyChanged();
}
}
public synchronized void enqueueMessage(OutgoingMessage message)
{
OutgoingMessage.ProcessingState state = message.getProcessingState();
if (state == OutgoingMessage.ProcessingState.Scheduled
|| state == OutgoingMessage.ProcessingState.None)
{
outgoingQueue.add(message);
message.setProcessingState(OutgoingMessage.ProcessingState.Queued);
notifyChanged();
maybeDequeueMessage();
}
}
private void notifyChanged()
{
app.sendBroadcast(new Intent(App.OUTBOX_CHANGED_INTENT));
}
public synchronized int size() {
return outgoingMessages.size();
}
public synchronized Collection<OutgoingMessage> getMessages()
{
return outgoingMessages.values();
}
}

View File

@ -23,7 +23,8 @@ public class OutgoingMessage extends QueuedMessage {
None, // not doing anything with this sms now... just sitting around None, // not doing anything with this sms now... just sitting around
Queued, // in the outgoing queue waiting to be sent Queued, // in the outgoing queue waiting to be sent
Sending, // passed to an expansion pack, waiting for status notification Sending, // passed to an expansion pack, waiting for status notification
Scheduled // waiting for a while before retrying after failure sending Scheduled, // waiting for a while before retrying after failure sending
Sent
} }
public OutgoingMessage(App app) public OutgoingMessage(App app)
@ -143,4 +144,29 @@ public class OutgoingMessage extends QueuedMessage {
intent.setData(this.getUri()); intent.setData(this.getUri());
return intent; return intent;
} }
public String getStatusText()
{
switch (state)
{
case Scheduled:
return "scheduled retry";
case Queued:
return "queued to send";
case Sending:
return "sending";
default:
return "";
}
}
public String getDescription()
{
return getDisplayType() + " to " + getTo();
}
public String getDisplayType()
{
return "SMS";
}
} }

View File

@ -6,12 +6,13 @@ 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 android.os.SystemClock;
import java.util.Date;
public abstract class QueuedMessage public abstract class QueuedMessage
{ {
protected long nextRetryTime = 0; protected long nextRetryTime = 0;
protected int numRetries = 0; protected int numRetries = 0;
protected Date dateCreated = new Date();
public App app; public App app;
public QueuedMessage(App app) public QueuedMessage(App app)
@ -19,6 +20,16 @@ public abstract class QueuedMessage
this.app = app; this.app = app;
} }
public Date getDateCreated()
{
return dateCreated;
}
public int getNumRetries()
{
return numRetries;
}
public boolean canRetryNow() { public boolean canRetryNow() {
return (nextRetryTime > 0 && nextRetryTime < SystemClock.elapsedRealtime()); return (nextRetryTime > 0 && nextRetryTime < SystemClock.elapsedRealtime());
} }
@ -64,6 +75,10 @@ public abstract class QueuedMessage
return true; return true;
} }
public abstract String getDisplayType();
public abstract String getDescription();
public abstract String getStatusText();
public abstract Uri getUri(); public abstract Uri getUri();
protected abstract Intent getRetryIntent(); protected abstract Intent getRetryIntent();

View File

@ -4,8 +4,6 @@ package org.envaya.sms.receiver;
import android.content.BroadcastReceiver; import android.content.BroadcastReceiver;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import org.envaya.sms.App; import org.envaya.sms.App;
public class ConnectivityChangeReceiver extends BroadcastReceiver { public class ConnectivityChangeReceiver extends BroadcastReceiver {

View File

@ -16,6 +16,6 @@ public class DequeueOutgoingMessageReceiver extends BroadcastReceiver {
return; return;
} }
app.maybeDequeueOutgoingMessage(); app.outbox.maybeDequeueMessage();
} }
} }

View File

@ -5,6 +5,7 @@ import android.content.BroadcastReceiver;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import org.envaya.sms.App; import org.envaya.sms.App;
import org.envaya.sms.IncomingMessage;
public class IncomingMessageRetry extends BroadcastReceiver public class IncomingMessageRetry extends BroadcastReceiver
{ {
@ -17,6 +18,13 @@ public class IncomingMessageRetry extends BroadcastReceiver
return; return;
} }
app.retryIncomingMessage(intent.getData()); IncomingMessage message = app.inbox.getMessage(intent.getData());
if (message == null)
{
return;
}
app.inbox.retryForwardMessage(message);
} }
} }

View File

@ -4,12 +4,15 @@
*/ */
package org.envaya.sms.receiver; package org.envaya.sms.receiver;
import android.app.Activity;
import android.content.BroadcastReceiver; import android.content.BroadcastReceiver;
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.Bundle; import android.os.Bundle;
import android.telephony.SmsManager;
import org.envaya.sms.App; import org.envaya.sms.App;
import org.envaya.sms.OutgoingMessage;
public class MessageStatusNotifier extends BroadcastReceiver { public class MessageStatusNotifier extends BroadcastReceiver {
@ -20,18 +23,56 @@ public class MessageStatusNotifier extends BroadcastReceiver {
Bundle extras = intent.getExtras(); Bundle extras = intent.getExtras();
int index = extras.getInt(App.STATUS_EXTRA_INDEX); int index = extras.getInt(App.STATUS_EXTRA_INDEX);
int numParts = extras.getInt(App.STATUS_EXTRA_NUM_PARTS); //int numParts = extras.getInt(App.STATUS_EXTRA_NUM_PARTS);
OutgoingMessage sms = app.outbox.getMessage(uri);
if (sms == null) {
return;
}
if (index != 0)
{
// TODO: process message status for parts other than the first one
return;
}
int resultCode = getResultCode(); int resultCode = getResultCode();
// uncomment to test retry on outgoing message failure
/* /*
// uncomment to test retry on outgoing message failure
if (Math.random() > 0.4) if (Math.random() > 0.4)
{ {
resultCode = SmsManager.RESULT_ERROR_NO_SERVICE; resultCode = SmsManager.RESULT_ERROR_NO_SERVICE;
} }
*/ */
app.notifyOutgoingMessageStatus(uri, resultCode, index, numParts); if (resultCode == Activity.RESULT_OK)
{
app.outbox.messageSent(sms);
}
else
{
app.outbox.messageFailed(sms, getErrorMessage(resultCode));
}
}
public String getErrorMessage(int resultCode)
{
switch (resultCode) {
case Activity.RESULT_OK:
return "";
case SmsManager.RESULT_ERROR_GENERIC_FAILURE:
return "generic failure";
case SmsManager.RESULT_ERROR_RADIO_OFF:
return "radio off";
case SmsManager.RESULT_ERROR_NO_SERVICE:
return "no service";
case SmsManager.RESULT_ERROR_NULL_PDU:
return "null PDU";
default:
return "unknown error";
}
} }
} }

View File

@ -5,6 +5,7 @@ import android.content.BroadcastReceiver;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import org.envaya.sms.App; import org.envaya.sms.App;
import org.envaya.sms.OutgoingMessage;
public class OutgoingMessageRetry extends BroadcastReceiver public class OutgoingMessageRetry extends BroadcastReceiver
{ {
@ -16,6 +17,13 @@ public class OutgoingMessageRetry extends BroadcastReceiver
{ {
return; return;
} }
app.retryOutgoingMessage(intent.getData());
OutgoingMessage message = app.outbox.getMessage(intent.getData());
if (message == null)
{
return;
}
app.outbox.enqueueMessage(message);
} }
} }

View File

@ -29,7 +29,7 @@ public class SmsReceiver extends BroadcastReceiver {
if (sms.isForwardable()) if (sms.isForwardable())
{ {
app.forwardToServer(sms); app.inbox.forwardMessage(sms);
if (!app.getKeepInInbox()) if (!app.getKeepInInbox())
{ {

View File

@ -28,14 +28,13 @@ public class ForwarderTask extends HttpTask {
protected void handleResponse(HttpResponse response) throws Exception { protected void handleResponse(HttpResponse response) throws Exception {
for (OutgoingMessage reply : parseResponseXML(response)) { for (OutgoingMessage reply : parseResponseXML(response)) {
app.sendOutgoingMessage(reply); app.outbox.sendMessage(reply);
} }
app.inbox.messageForwarded(message);
app.setIncomingMessageStatus(message, true);
} }
@Override @Override
protected void handleFailure() { protected void handleFailure() {
app.setIncomingMessageStatus(message, false); app.inbox.messageFailed(message);
} }
} }

View File

@ -15,7 +15,7 @@ public class PollerTask extends HttpTask {
@Override @Override
protected void handleResponse(HttpResponse response) throws Exception { protected void handleResponse(HttpResponse response) throws Exception {
for (OutgoingMessage reply : parseResponseXML(response)) { for (OutgoingMessage reply : parseResponseXML(response)) {
app.sendOutgoingMessage(reply); app.outbox.sendMessage(reply);
} }
} }
} }

View File

@ -1,105 +0,0 @@
package org.envaya.sms.ui;
// from http://www.marvinlabs.com/2010/10/custom-listview-ability-check-items/
// package fr.marvinlabs.widget;
import java.util.ArrayList;
import java.util.List;
import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Checkable;
import android.widget.RelativeLayout;
/**
* Extension of a relative layout to provide a checkable behavior
*
* @author marvinlabs
*/
public class CheckableRelativeLayout extends RelativeLayout implements
Checkable {
private boolean isChecked;
private List<Checkable> checkableViews;
public CheckableRelativeLayout(Context context, AttributeSet attrs,
int defStyle) {
super(context, attrs, defStyle);
initialise(attrs);
}
public CheckableRelativeLayout(Context context, AttributeSet attrs) {
super(context, attrs);
initialise(attrs);
}
public CheckableRelativeLayout(Context context, int checkableId) {
super(context);
initialise(null);
}
/*
* @see android.widget.Checkable#isChecked()
*/
public boolean isChecked() {
return isChecked;
}
/*
* @see android.widget.Checkable#setChecked(boolean)
*/
public void setChecked(boolean isChecked) {
this.isChecked = isChecked;
for (Checkable c : checkableViews) {
c.setChecked(isChecked);
}
}
/*
* @see android.widget.Checkable#toggle()
*/
public void toggle() {
this.isChecked = !this.isChecked;
for (Checkable c : checkableViews) {
c.toggle();
}
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
final int childCount = this.getChildCount();
for (int i = 0; i < childCount; ++i) {
findCheckableChildren(this.getChildAt(i));
}
}
/**
* Read the custom XML attributes
*/
private void initialise(AttributeSet attrs) {
this.isChecked = false;
this.checkableViews = new ArrayList<Checkable>(5);
}
/**
* Add to our checkable list all the children of the view that implement the
* interface Checkable
*/
private void findCheckableChildren(View v) {
if (v instanceof Checkable) {
this.checkableViews.add((Checkable) v);
}
if (v instanceof ViewGroup) {
final ViewGroup vg = (ViewGroup) v;
final int childCount = vg.getChildCount();
for (int i = 0; i < childCount; ++i) {
findCheckableChildren(vg.getChildAt(i));
}
}
}
}

View File

@ -1,86 +0,0 @@
package org.envaya.sms.ui;
import android.app.ListActivity;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.util.SparseBooleanArray;
import android.view.View;
import android.widget.ListView;
import android.widget.SimpleCursorAdapter;
import org.envaya.sms.App;
import org.envaya.sms.IncomingMessage;
import org.envaya.sms.IncomingSms;
import org.envaya.sms.R;
public class ForwardInbox extends ListActivity {
private App app;
private Cursor cur;
/** Called when the activity is first created. */
@Override
public void onCreate(Bundle icicle) {
super.onCreate(icicle);
app = (App) getApplication();
setContentView(R.layout.inbox);
// undocumented API; see
// core/java/android/provider/Telephony.java
Uri inboxUri = Uri.parse("content://sms/inbox");
cur = getContentResolver().query(inboxUri, null, null, null, null);
SimpleCursorAdapter adapter = new SimpleCursorAdapter(this,
R.layout.inbox_item,
cur,
new String[] {"address","body"},
new int[] {R.id.inbox_address, R.id.inbox_body});
setListAdapter(adapter);
ListView listView = getListView();
listView.setItemsCanFocus(false);
}
public void forwardSelected(View view) {
ListView listView = getListView();
SparseBooleanArray checkedItems = listView.getCheckedItemPositions();
int checkedItemsCount = checkedItems.size();
int addressIndex = cur.getColumnIndex("address");
int bodyIndex = cur.getColumnIndex("body");
int dateIndex = cur.getColumnIndex("date");
for (int i = 0; i < checkedItemsCount; ++i)
{
int position = checkedItems.keyAt(i);
boolean isChecked = checkedItems.valueAt(i);
if (isChecked)
{
cur.moveToPosition(position);
String address = cur.getString(addressIndex);
String body = cur.getString(bodyIndex);
long date = cur.getLong(dateIndex);
IncomingMessage sms = new IncomingSms(app, address, body, date);
app.forwardToServer(sms);
}
}
this.finish();
}
}

View File

@ -1,72 +0,0 @@
package org.envaya.sms.ui;
// from http://www.marvinlabs.com/2010/10/custom-listview-ability-check-items/
// package fr.marvinlabs.widget;
import android.content.Context;
import android.util.AttributeSet;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.widget.CheckBox;
/**
* CheckBox that does not react to any user event in order to let the container handle them.
*/
public class InertCheckBox extends CheckBox {
// Provide the same constructors as the superclass
public InertCheckBox(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
// Provide the same constructors as the superclass
public InertCheckBox(Context context, AttributeSet attrs) {
super(context, attrs);
}
// Provide the same constructors as the superclass
public InertCheckBox(Context context) {
super(context);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
// Make the checkbox not respond to any user event
return false;
}
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
// Make the checkbox not respond to any user event
return false;
}
@Override
public boolean onKeyMultiple(int keyCode, int repeatCount, KeyEvent event) {
// Make the checkbox not respond to any user event
return false;
}
@Override
public boolean onKeyPreIme(int keyCode, KeyEvent event) {
// Make the checkbox not respond to any user event
return false;
}
@Override
public boolean onKeyShortcut(int keyCode, KeyEvent event) {
// Make the checkbox not respond to any user event
return false;
}
@Override
public boolean onKeyUp(int keyCode, KeyEvent event) {
// Make the checkbox not respond to any user event
return false;
}
@Override
public boolean onTrackballEvent(MotionEvent event) {
// Make the checkbox not respond to any user event
return false;
}
}

View File

@ -15,12 +15,9 @@ import android.view.MenuItem;
import android.view.View; import android.view.View;
import android.widget.ScrollView; import android.widget.ScrollView;
import android.widget.TextView; import android.widget.TextView;
import java.util.List;
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.IncomingMms;
import org.envaya.sms.MmsUtils;
import org.envaya.sms.R; import org.envaya.sms.R;
public class Main extends Activity { public class Main extends Activity {
@ -76,7 +73,7 @@ public class Main extends Activity {
updateLogView(); updateLogView();
IntentFilter logReceiverFilter = new IntentFilter(); IntentFilter logReceiverFilter = new IntentFilter();
logReceiverFilter.addAction(App.LOG_INTENT); logReceiverFilter.addAction(App.LOG_CHANGED_INTENT);
registerReceiver(logReceiver, logReceiverFilter); registerReceiver(logReceiver, logReceiverFilter);
} }
@ -94,10 +91,10 @@ public class Main extends Activity {
app.retryStuckMessages(); app.retryStuckMessages();
return true; return true;
case R.id.forward_inbox: case R.id.forward_inbox:
startActivity(new Intent(this, ForwardInbox.class)); startActivity(new Intent(this, MessagingInbox.class));
return true; return true;
case R.id.help: case R.id.pending:
startActivity(new Intent(this, Help.class)); startActivity(new Intent(this, PendingMessages.class));
return true; return true;
case R.id.test: case R.id.test:
app.log("Testing server connection..."); app.log("Testing server connection...");
@ -119,10 +116,11 @@ public class Main extends Activity {
@Override @Override
public boolean onPrepareOptionsMenu(Menu menu) { public boolean onPrepareOptionsMenu(Menu menu) {
MenuItem item = menu.findItem(R.id.retry_now); MenuItem retryItem = menu.findItem(R.id.retry_now);
int stuckMessages = app.getStuckMessageCount(); int pendingMessages = app.getPendingMessageCount();
item.setEnabled(stuckMessages > 0); retryItem.setEnabled(pendingMessages > 0);
item.setTitle("Retry Fwd (" + stuckMessages + ")"); retryItem.setTitle("Retry All (" + pendingMessages + ")");
return true; return true;
} }

View File

@ -0,0 +1,146 @@
package org.envaya.sms.ui;
import android.app.AlertDialog;
import android.app.ListActivity;
import android.content.DialogInterface;
import android.content.DialogInterface.OnClickListener;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.ListView;
import android.widget.SimpleCursorAdapter;
import android.widget.Toast;
import org.envaya.sms.App;
import org.envaya.sms.IncomingMessage;
import org.envaya.sms.IncomingSms;
import org.envaya.sms.R;
public class MessagingInbox extends ListActivity {
private App app;
private Cursor cur;
/** Called when the activity is first created. */
@Override
public void onCreate(Bundle icicle) {
super.onCreate(icicle);
app = (App) getApplication();
setContentView(R.layout.inbox);
// 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");
SimpleCursorAdapter adapter = new SimpleCursorAdapter(this,
R.layout.inbox_item,
cur,
new String[] {"address","body"},
new int[] {R.id.inbox_address, R.id.inbox_body});
setListAdapter(adapter);
ListView listView = getListView();
listView.setOnItemClickListener(new OnItemClickListener() {
public void onItemClick(AdapterView<?> parent, View view,
int position, long id)
{
final IncomingMessage message = getMessageAtPosition(position);
final CharSequence[] options = {"Forward", "Cancel"};
new AlertDialog.Builder(MessagingInbox.this)
.setTitle(message.getDescription())
.setItems(options, new OnClickListener() {
public void onClick(DialogInterface dialog, int which)
{
if (which == 0)
{
app.inbox.forwardMessage(message);
showToast("Forwarding " + message.getDescription());
}
dialog.dismiss();
}
})
.show();
}
});
}
public void showToast(String text)
{
Toast.makeText(this, text, Toast.LENGTH_SHORT).show();
}
public IncomingMessage getMessageAtPosition(int position)
{
int addressIndex = cur.getColumnIndex("address");
int bodyIndex = cur.getColumnIndex("body");
int dateIndex = cur.getColumnIndex("date");
cur.moveToPosition(position);
String address = cur.getString(addressIndex);
String body = cur.getString(bodyIndex);
long date = cur.getLong(dateIndex);
return new IncomingSms(app, address, body, date);
}
public void forwardAllClicked() {
final int count = cur.getCount();
for (int i = 0; i < count; ++i)
{
app.inbox.forwardMessage(getMessageAtPosition(i));
}
finish();
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
// Handle item selection
switch (item.getItemId()) {
case R.id.forward_all:
forwardAllClicked();
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.inbox, menu);
return(true);
}
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
MenuItem forwardItem = menu.findItem(R.id.forward_all);
int numMessages = cur.getCount();
forwardItem.setEnabled(numMessages > 0);
forwardItem.setTitle("Forward All (" + numMessages + ")");
return true;
}
}

View File

@ -0,0 +1,274 @@
package org.envaya.sms.ui;
import android.app.AlertDialog;
import android.app.ListActivity;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.DialogInterface;
import android.content.DialogInterface.OnClickListener;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.ListView;
import android.widget.TextView;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
import org.envaya.sms.App;
import org.envaya.sms.IncomingMessage;
import org.envaya.sms.OutgoingMessage;
import org.envaya.sms.QueuedMessage;
import org.envaya.sms.R;
public class PendingMessages extends ListActivity {
private App app;
private List<QueuedMessage> displayedMessages;
private BroadcastReceiver refreshReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
refreshMessages();
}
};
/** Called when the activity is first created. */
@Override
public void onCreate(Bundle icicle) {
super.onCreate(icicle);
app = (App) getApplication();
setContentView(R.layout.pending_messages);
IntentFilter refreshReceiverFilter = new IntentFilter();
refreshReceiverFilter.addAction(App.INBOX_CHANGED_INTENT);
refreshReceiverFilter.addAction(App.OUTBOX_CHANGED_INTENT);
registerReceiver(refreshReceiver, refreshReceiverFilter);
ListView listView = getListView();
listView.setOnItemClickListener(new OnItemClickListener() {
public void onItemClick(AdapterView<?> parent, View view,
int position, long id)
{
final QueuedMessage message = displayedMessages.get(position);
final CharSequence[] options = {"Retry", "Delete", "Cancel"};
new AlertDialog.Builder(PendingMessages.this)
.setTitle(message.getDescription())
.setItems(options, new OnClickListener() {
public void onClick(DialogInterface dialog, int which)
{
if (which == 0)
{
retryMessage(message);
}
else if (which == 1)
{
deleteMessage(message);
}
dialog.dismiss();
}
})
.show();
}
});
refreshMessages();
}
public void refreshMessages()
{
final ArrayList<QueuedMessage> messages = new ArrayList<QueuedMessage>();
synchronized(app.outbox)
{
for (OutgoingMessage message : app.outbox.getMessages())
{
messages.add(message);
}
}
synchronized(app.inbox)
{
for (IncomingMessage message : app.inbox.getMessages())
{
messages.add(message);
}
}
Collections.sort(messages, new Comparator<QueuedMessage>(){
public int compare(QueuedMessage t1, QueuedMessage t2)
{
return t1.getDateCreated().compareTo(t2.getDateCreated());
}
});
displayedMessages = messages;
final LayoutInflater inflater = (LayoutInflater)getSystemService(Context.LAYOUT_INFLATER_SERVICE);
final DateFormat longFormat = new SimpleDateFormat("dd MMM hh:mm:ss");
final DateFormat shortFormat = new SimpleDateFormat("hh:mm:ss");
final Date now = new Date();
ArrayAdapter<QueuedMessage> arrayAdapter = new ArrayAdapter<QueuedMessage>(this,
R.layout.pending_message,
messages.toArray(new QueuedMessage[]{})) {
@Override
public View getView(int position, View convertView, ViewGroup parent) {
View v = convertView;
if (v == null) {
v = inflater.inflate(R.layout.pending_message, null);
}
QueuedMessage message = messages.get(position);
if (message == null)
{
return null;
}
TextView addr = (TextView) v.findViewById(R.id.pending_address);
TextView time = (TextView) v.findViewById(R.id.pending_time);
TextView status = (TextView) v.findViewById(R.id.pending_status);
addr.setText(message.getDescription());
String statusText = message.getStatusText();
int numRetries = message.getNumRetries();
if (numRetries > 0)
{
statusText = statusText + " (tries=" + numRetries + ")";
}
status.setText(statusText);
Date date = message.getDateCreated();
DateFormat format =
(date.getDate() == now.getDate() && date.getMonth() == now.getMonth())
? shortFormat : longFormat;
time.setText(format.format(date));
return v;
}
};
setListAdapter(arrayAdapter);
}
public void deleteMessage(QueuedMessage message)
{
if (message instanceof IncomingMessage)
{
app.inbox.deleteMessage((IncomingMessage)message);
}
else
{
app.outbox.deleteMessage((OutgoingMessage)message);
}
}
public void deleteAll()
{
for (QueuedMessage message : displayedMessages)
{
deleteMessage(message);
}
}
public void deleteAllClicked() {
new AlertDialog.Builder(this)
.setTitle("Confirm Action")
.setMessage("Do you want to delete all "+displayedMessages.size()+" pending messages?")
.setPositiveButton("OK",
new OnClickListener() {
public void onClick(DialogInterface dialog, int which)
{
dialog.dismiss();
deleteAll();
}
}
)
.setNegativeButton("Cancel",
new OnClickListener() {
public void onClick(DialogInterface dialog, int which)
{
dialog.dismiss();
}
}
)
.show();
}
public void retryMessage(QueuedMessage message)
{
if (message instanceof IncomingMessage)
{
app.inbox.retryForwardMessage((IncomingMessage)message);
}
else
{
app.outbox.enqueueMessage((OutgoingMessage)message);
}
}
public void retryAllClicked()
{
for (QueuedMessage message : displayedMessages)
{
retryMessage(message);
}
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
// Handle item selection
switch (item.getItemId()) {
case R.id.retry_all:
retryAllClicked();
return true;
case R.id.delete_all:
deleteAllClicked();
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.pending_messages, menu);
return(true);
}
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
MenuItem retryItem = menu.findItem(R.id.retry_all);
MenuItem deleteItem = menu.findItem(R.id.delete_all);
int numMessages = displayedMessages.size();
retryItem.setEnabled(numMessages > 0);
deleteItem.setEnabled(numMessages > 0);
return true;
}
}

View File

@ -16,11 +16,15 @@ import org.envaya.sms.R;
public class Prefs extends PreferenceActivity implements OnSharedPreferenceChangeListener { public class Prefs extends PreferenceActivity implements OnSharedPreferenceChangeListener {
private App app;
@Override @Override
public void onCreate(Bundle savedInstanceState) { public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
addPreferencesFromResource(R.xml.prefs); addPreferencesFromResource(R.xml.prefs);
app = (App) getApplication();
PreferenceScreen screen = this.getPreferenceScreen(); PreferenceScreen screen = this.getPreferenceScreen();
int numPrefs = screen.getPreferenceCount(); int numPrefs = screen.getPreferenceCount();
@ -46,7 +50,6 @@ public class Prefs extends PreferenceActivity implements OnSharedPreferenceChang
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
App app = (App) getApplication();
if (key.equals("outgoing_interval")) if (key.equals("outgoing_interval"))
{ {
@ -109,7 +112,9 @@ public class Prefs extends PreferenceActivity implements OnSharedPreferenceChang
private void updatePrefSummary(Preference p) private void updatePrefSummary(Preference p)
{ {
if ("wifi_sleep_policy".equals(p.getKey())) String key = p.getKey();
if ("wifi_sleep_policy".equals(key))
{ {
int sleepPolicy; int sleepPolicy;
@ -136,6 +141,10 @@ public class Prefs extends PreferenceActivity implements OnSharedPreferenceChang
break; break;
} }
} }
else if ("help".equals(key))
{
p.setSummary(app.getPackageInfo().versionName);
}
else if (p instanceof ListPreference) { else if (p instanceof ListPreference) {
p.setSummary(((ListPreference)p).getEntry()); p.setSummary(((ListPreference)p).getEntry());
} }