4
0
mirror of https://github.com/cwinfo/envayasms.git synced 2025-07-03 05:37:44 +00:00

rename to EnvayaSMS; add icon

This commit is contained in:
Jesse Young
2011-09-22 16:16:46 -07:00
parent 73bc3c9fc6
commit 7df89cd5d0
44 changed files with 201 additions and 189 deletions

692
src/org/envaya/sms/App.java Executable file
View File

@ -0,0 +1,692 @@
package org.envaya.sms;
import android.app.Activity;
import android.app.AlarmManager;
import android.app.Application;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager.NameNotFoundException;
import android.net.Uri;
import android.os.Bundle;
import android.os.SystemClock;
import android.preference.PreferenceManager;
import android.telephony.SmsManager;
import android.text.Html;
import android.text.SpannableStringBuilder;
import android.util.Log;
import java.text.DateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.http.client.HttpClient;
import org.apache.http.conn.scheme.PlainSocketFactory;
import org.apache.http.conn.scheme.Scheme;
import org.apache.http.conn.scheme.SchemeRegistry;
import org.apache.http.conn.ssl.SSLSocketFactory;
import org.apache.http.impl.client.DefaultHttpClient;
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.HttpConnectionParams;
import org.apache.http.params.HttpParams;
import org.apache.http.params.HttpProtocolParams;
import org.envaya.sms.receiver.OutgoingMessagePoller;
import org.envaya.sms.task.HttpTask;
import org.envaya.sms.task.PollerTask;
import org.json.JSONArray;
import org.json.JSONException;
public final class App extends Application {
public static final String ACTION_OUTGOING = "outgoing";
public static final String ACTION_INCOMING = "incoming";
public static final String ACTION_SEND_STATUS = "send_status";
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 MESSAGE_TYPE_MMS = "mms";
public static final String MESSAGE_TYPE_SMS = "sms";
public static final String LOG_NAME = "EnvayaSMS";
// 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 QUERY_EXPANSION_PACKS_INTENT = "org.envaya.sms.QUERY_EXPANSION_PACKS";
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";
public static final String OUTGOING_SMS_EXTRA_TO = "to";
public static final String OUTGOING_SMS_EXTRA_BODY = "body";
public static final int OUTGOING_SMS_UNHANDLED = Activity.RESULT_FIRST_USER;
// intent for MessageStatusNotifier to receive status updates for outgoing SMS
// (even if sent by an expansion pack)
public static final String MESSAGE_STATUS_INTENT = "org.envaya.sms.MESSAGE_STATUS";
public static final int MAX_DISPLAYED_LOG = 4000;
public static final int LOG_TIMESTAMP_INTERVAL = 60000;
// 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)
public static final Uri CONTENT_URI = Uri.parse("content://org.envaya.sms");
public static final Uri INCOMING_URI = Uri.withAppendedPath(CONTENT_URI, "incoming");
public static final Uri OUTGOING_URI = Uri.withAppendedPath(CONTENT_URI, "outgoing");
// 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
// between when we prepare messages and when SMSDispatcher receives them
public static int OUTGOING_SMS_CHECK_PERIOD = 3605000; // one hour plus 5 sec (in ms)
public static int OUTGOING_SMS_MAX_COUNT = 100;
private Map<Uri, IncomingMessage> incomingMessages = new HashMap<Uri, IncomingMessage>();
private Map<Uri, OutgoingMessage> outgoingMessages = new HashMap<Uri, OutgoingMessage>();
private SharedPreferences settings;
private MmsObserver mmsObserver;
private SpannableStringBuilder displayedLog = new SpannableStringBuilder();
private long lastLogTime;
private PackageInfo packageInfo;
// list of package names (e.g. org.envaya.sms, or org.envaya.sms.packXX)
// for this package and all expansion packs
private List<String> outgoingMessagePackages = new ArrayList<String>();
// count to provide round-robin selection of expansion packs
private int outgoingMessageCount = -1;
// map of package name => sorted list of timestamps of outgoing messages
private HashMap<String, ArrayList<Long>> outgoingTimestamps
= new HashMap<String, ArrayList<Long>>();
private MmsUtils mmsUtils;
@Override
public void onCreate()
{
super.onCreate();
settings = PreferenceManager.getDefaultSharedPreferences(this);
mmsUtils = new MmsUtils(this);
outgoingMessagePackages.add(getPackageName());
mmsObserver = new MmsObserver(this);
try
{
packageInfo = getPackageManager().getPackageInfo(getPackageName(), 0);
}
catch (NameNotFoundException ex)
{
// should not happen
logError("Error finding package info", ex);
return;
}
updateExpansionPacks();
log(Html.fromHtml(
isEnabled() ? "<b>SMS gateway running.</b>" : "<b>SMS gateway disabled.</b>"));
log("Server URL is: " + getDisplayString(getServerUrl()));
log("Your phone number is: " + getDisplayString(getPhoneNumber()));
if (isTestMode())
{
log("Test mode is ON");
log("Test phone numbers:");
for (String sender : getTestPhoneNumbers())
{
log(" " + sender);
}
}
enabledChanged();
log(Html.fromHtml("<b>Press Menu to edit settings.</b>"));
}
public void enabledChanged()
{
if (isEnabled())
{
mmsObserver.register();
}
else
{
mmsObserver.unregister();
}
setOutgoingMessageAlarm();
startService(new Intent(this, ForegroundService.class));
}
public PackageInfo getPackageInfo()
{
return packageInfo;
}
public synchronized String chooseOutgoingSmsPackage()
{
outgoingMessageCount++;
int numPackages = outgoingMessagePackages.size();
// round robin selection of packages that are under max sending rate
for (int i = 0; i < numPackages; i++)
{
int packageIndex = (outgoingMessageCount + i) % numPackages;
String packageName = outgoingMessagePackages.get(packageIndex);
// implement rate-limiting algorithm from
// com.android.internal.telephony.SMSDispatcher.SmsCounter
if (!outgoingTimestamps.containsKey(packageName)) {
outgoingTimestamps.put(packageName, new ArrayList<Long>());
}
ArrayList<Long> sent = outgoingTimestamps.get(packageName);
Long ct = System.currentTimeMillis();
//log(packageName + " SMS send size=" + sent.size());
// remove old timestamps
while (sent.size() > 0 && (ct - sent.get(0)) > OUTGOING_SMS_CHECK_PERIOD )
{
sent.remove(0);
}
if ( (sent.size() + 1) <= OUTGOING_SMS_MAX_COUNT)
{
sent.add(ct);
return packageName;
}
}
log("Can't send outgoing SMS: maximum limit of "
+ getOutgoingMessageLimit() + " in 1 hour reached");
log("To increase this limit, install an expansion pack.");
return null;
}
private synchronized void setExpansionPacks(List<String> packages)
{
int prevLimit = getOutgoingMessageLimit();
if (packages == null)
{
packages = new ArrayList<String>();
}
packages.add(getPackageName());
outgoingMessagePackages = packages;
int newLimit = getOutgoingMessageLimit();
if (prevLimit != newLimit)
{
log("Outgoing SMS limit: " + newLimit + " messages/hour");
}
}
public int getOutgoingMessageLimit()
{
return outgoingMessagePackages.size() * OUTGOING_SMS_MAX_COUNT;
}
public void updateExpansionPacks()
{
ArrayList<String> packages = new ArrayList<String>();
Bundle extras = new Bundle();
extras.putStringArrayList(App.QUERY_EXPANSION_PACKS_EXTRA_PACKAGES, packages);
sendOrderedBroadcast(
new Intent(App.QUERY_EXPANSION_PACKS_INTENT),
"android.permission.SEND_SMS",
new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent resultIntent) {
setExpansionPacks(this.getResultExtras(false)
.getStringArrayList(App.QUERY_EXPANSION_PACKS_EXTRA_PACKAGES));
}
},
null,
Activity.RESULT_OK,
null,
extras);
}
public void checkOutgoingMessages()
{
String serverUrl = getServerUrl();
if (serverUrl.length() > 0) {
log("Checking for outgoing messages");
new PollerTask(this).execute();
} else {
log("Can't check outgoing messages; server URL not set");
}
}
public void setOutgoingMessageAlarm() {
AlarmManager alarm = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
PendingIntent pendingIntent = PendingIntent.getBroadcast(this,
0,
new Intent(this, OutgoingMessagePoller.class),
0);
alarm.cancel(pendingIntent);
int pollSeconds = getOutgoingPollSeconds();
if (isEnabled())
{
if (pollSeconds > 0) {
alarm.setRepeating(
AlarmManager.ELAPSED_REALTIME_WAKEUP,
SystemClock.elapsedRealtime(),
pollSeconds * 1000,
pendingIntent);
log("Checking for outgoing messages every " + pollSeconds + " sec");
} else {
log("Not checking for outgoing messages.");
}
}
}
public String getDisplayString(String str) {
if (str.length() == 0) {
return "(not set)";
} else {
return str;
}
}
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);
}
public boolean isTestMode()
{
return settings.getBoolean("test_mode", false);
}
public boolean getKeepInInbox()
{
return settings.getBoolean("keep_in_inbox", false);
}
public String getPassword() {
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() {
retryStuckOutgoingMessages();
retryStuckIncomingMessages();
}
public synchronized int getStuckMessageCount() {
return outgoingMessages.size() + incomingMessages.size();
}
public synchronized void retryStuckOutgoingMessages() {
for (OutgoingMessage sms : outgoingMessages.values()) {
sms.retryNow();
}
}
public synchronized void retryStuckIncomingMessages() {
for (IncomingMessage sms : incomingMessages.values()) {
sms.retryNow();
}
}
public synchronized void setIncomingMessageStatus(IncomingMessage message, boolean success) {
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())
{
incomingMessages.remove(uri);
}
}
public synchronized void notifyOutgoingMessageStatus(Uri uri, int resultCode) {
OutgoingMessage sms = outgoingMessages.get(uri);
if (sms == null) {
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;
}
switch (resultCode) {
case SmsManager.RESULT_ERROR_GENERIC_FAILURE:
case SmsManager.RESULT_ERROR_RADIO_OFF:
case SmsManager.RESULT_ERROR_NO_SERVICE:
if (!sms.scheduleRetry()) {
outgoingMessages.remove(uri);
}
break;
default:
outgoingMessages.remove(uri);
break;
}
}
public synchronized void sendOutgoingMessage(OutgoingMessage sms) {
if (isTestMode() && !isTestPhoneNumber(sms.getTo()))
{
// this is mostly to prevent accidentally sending real messages to
// random people while testing...
log("Ignoring outgoing SMS to " + sms.getTo());
return;
}
Uri uri = sms.getUri();
if (outgoingMessages.containsKey(uri)) {
log("Duplicate outgoing " + sms.getLogName() + ", skipping");
return;
}
outgoingMessages.put(uri, sms);
log("Sending " + sms.getLogName() + " to " + sms.getTo());
sms.trySend();
}
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());
message.tryForwardToServer();
}
public synchronized void retryIncomingMessage(Uri uri) {
IncomingMessage message = incomingMessages.get(uri);
if (message != null) {
message.retryNow();
}
}
public synchronized void retryOutgoingMessage(Uri uri) {
OutgoingMessage sms = outgoingMessages.get(uri);
if (sms != null) {
sms.retryNow();
}
}
public void debug(String msg) {
Log.d(LOG_NAME, msg);
}
public synchronized void log(CharSequence msg)
{
Log.d(LOG_NAME, msg.toString());
// prevent displayed log from growing too big
int length = displayedLog.length();
if (length > MAX_DISPLAYED_LOG)
{
int startPos = length - MAX_DISPLAYED_LOG * 3 / 4;
for (int cur = startPos; cur < startPos + 100 && cur < length; cur++)
{
if (displayedLog.charAt(cur) == '\n')
{
startPos = cur;
break;
}
}
displayedLog.replace(0, startPos, "[Older log messages not shown]\n");
}
// display a timestamp in the log occasionally
long logTime = SystemClock.elapsedRealtime();
if (logTime - lastLogTime > LOG_TIMESTAMP_INTERVAL)
{
Date date = new Date();
displayedLog.append("[" + DateFormat.getTimeInstance().format(date) + "]\n");
lastLogTime = logTime;
}
displayedLog.append(msg);
displayedLog.append("\n");
Intent broadcast = new Intent(App.LOG_INTENT);
sendBroadcast(broadcast);
}
public synchronized CharSequence getDisplayedLog()
{
return displayedLog;
}
public void logError(Throwable ex) {
logError("ERROR", ex);
}
public void logError(String msg, Throwable ex) {
logError(msg, ex, false);
}
public void logError(String msg, Throwable ex, boolean detail) {
log(msg + ": " + ex.getClass().getName() + ": " + ex.getMessage());
if (detail) {
for (StackTraceElement elem : ex.getStackTrace()) {
log(elem.getClassName() + ":" + elem.getMethodName() + ":" + elem.getLineNumber());
}
Throwable innerEx = ex.getCause();
if (innerEx != null) {
logError("Inner exception:", innerEx, true);
}
}
}
public MmsUtils getMmsUtils()
{
return mmsUtils;
}
private List<String> testPhoneNumbers;
public List<String> getTestPhoneNumbers()
{
if (testPhoneNumbers == null)
{
testPhoneNumbers = new ArrayList<String>();
String phoneNumbersJson = settings.getString("test_phone_numbers", "");
if (phoneNumbersJson.length() > 0)
{
try
{
JSONArray arr = new JSONArray(phoneNumbersJson);
int numSenders = arr.length();
for (int i = 0; i < numSenders; i++)
{
testPhoneNumbers.add(arr.getString(i));
}
}
catch (JSONException ex)
{
logError("Error parsing test phone numbers", ex);
}
}
}
return testPhoneNumbers;
}
public void addTestPhoneNumber(String phoneNumber)
{
List<String> phoneNumbers = getTestPhoneNumbers();
log("Added test phone number: " + phoneNumber);
phoneNumbers.add(phoneNumber);
saveTestPhoneNumbers(phoneNumbers);
}
public void removeTestPhoneNumber(String phoneNumber)
{
List<String> phoneNumbers = getTestPhoneNumbers();
phoneNumbers.remove(phoneNumber);
log("Removed test phone number: " + phoneNumber);
saveTestPhoneNumbers(phoneNumbers);
}
private void saveTestPhoneNumbers(List<String> phoneNumbers)
{
settings.edit().putString("test_phone_numbers",
new JSONArray(phoneNumbers).toString()
).commit();
}
public boolean isTestPhoneNumber(String phoneNumber)
{
for (String testNumber : getTestPhoneNumbers())
{
// handle inexactness due to various different ways of formatting numbers
if (testNumber.contains(phoneNumber) || phoneNumber.contains(testNumber))
{
return true;
}
}
return false;
}
private HttpClient httpClient;
public HttpParams getDefaultHttpParams()
{
HttpParams httpParams = new BasicHttpParams();
HttpConnectionParams.setConnectionTimeout(httpParams, 8000);
HttpConnectionParams.setSoTimeout(httpParams, 8000);
HttpProtocolParams.setContentCharset(httpParams, "UTF-8");
return httpParams;
}
public synchronized HttpClient getHttpClient()
{
if (httpClient == null)
{
// via http://thinkandroid.wordpress.com/2009/12/31/creating-an-http-client-example/
// also http://hc.apache.org/httpclient-3.x/threading.html
HttpParams httpParams = getDefaultHttpParams();
SchemeRegistry registry = new SchemeRegistry();
registry.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80));
final SSLSocketFactory sslSocketFactory = SSLSocketFactory.getSocketFactory();
sslSocketFactory.setHostnameVerifier(SSLSocketFactory.BROWSER_COMPATIBLE_HOSTNAME_VERIFIER);
registry.register(new Scheme("https", sslSocketFactory, 443));
ThreadSafeClientConnManager manager = new ThreadSafeClientConnManager(httpParams, registry);
httpClient = new DefaultHttpClient(manager, httpParams);
}
return httpClient;
}
}

View File

@ -0,0 +1,269 @@
// Copyright 2003-2010 Christian d'Heureuse, Inventec Informatik AG, Zurich, Switzerland
// www.source-code.biz, www.inventec.ch/chdh
//
// This module is multi-licensed and may be used under the terms
// of any of the following licenses:
//
// EPL, Eclipse Public License, V1.0 or later, http://www.eclipse.org/legal
// LGPL, GNU Lesser General Public License, V2.1 or later, http://www.gnu.org/licenses/lgpl.html
// GPL, GNU General Public License, V2 or later, http://www.gnu.org/licenses/gpl.html
// AL, Apache License, V2.0 or later, http://www.apache.org/licenses
// BSD, BSD License, http://www.opensource.org/licenses/bsd-license.php
// MIT, MIT License, http://www.opensource.org/licenses/MIT
//
// Please contact the author if you need another license.
// This module is provided "as is", without warranties of any kind.
package org.envaya.sms;
/**
* A Base64 encoder/decoder.
*
* <p>
* This class is used to encode and decode data in Base64 format as described in RFC 1521.
*
* <p>
* Project home page: <a href="http://www.source-code.biz/base64coder/java/">www.source-code.biz/base64coder/java</a><br>
* Author: Christian d'Heureuse, Inventec Informatik AG, Zurich, Switzerland<br>
* Multi-licensed: EPL / LGPL / GPL / AL / BSD / MIT.
*/
public class Base64Coder {
// The line separator string of the operating system.
private static final String systemLineSeparator = System.getProperty("line.separator");
// Mapping table from 6-bit nibbles to Base64 characters.
private static final char[] map1 = new char[64];
static {
int i = 0;
for (char c = 'A'; c <= 'Z'; c++) {
map1[i++] = c;
}
for (char c = 'a'; c <= 'z'; c++) {
map1[i++] = c;
}
for (char c = '0'; c <= '9'; c++) {
map1[i++] = c;
}
map1[i++] = '+';
map1[i++] = '/';
}
// Mapping table from Base64 characters to 6-bit nibbles.
private static final byte[] map2 = new byte[128];
static {
for (int i = 0; i < map2.length; i++) {
map2[i] = -1;
}
for (int i = 0; i < 64; i++) {
map2[map1[i]] = (byte) i;
}
}
/**
* Encodes a string into Base64 format.
* No blanks or line breaks are inserted.
* @param s A String to be encoded.
* @return A String containing the Base64 encoded data.
*/
public static String encodeString(String s) {
return new String(encode(s.getBytes()));
}
/**
* Encodes a byte array into Base 64 format and breaks the output into lines of 76 characters.
* This method is compatible with <code>sun.misc.BASE64Encoder.encodeBuffer(byte[])</code>.
* @param in An array containing the data bytes to be encoded.
* @return A String containing the Base64 encoded data, broken into lines.
*/
public static String encodeLines(byte[] in) {
return encodeLines(in, 0, in.length, 76, systemLineSeparator);
}
/**
* Encodes a byte array into Base 64 format and breaks the output into lines.
* @param in An array containing the data bytes to be encoded.
* @param iOff Offset of the first byte in <code>in</code> to be processed.
* @param iLen Number of bytes to be processed in <code>in</code>, starting at <code>iOff</code>.
* @param lineLen Line length for the output data. Should be a multiple of 4.
* @param lineSeparator The line separator to be used to separate the output lines.
* @return A String containing the Base64 encoded data, broken into lines.
*/
public static String encodeLines(byte[] in, int iOff, int iLen, int lineLen, String lineSeparator) {
int blockLen = (lineLen * 3) / 4;
if (blockLen <= 0) {
throw new IllegalArgumentException();
}
int lines = (iLen + blockLen - 1) / blockLen;
int bufLen = ((iLen + 2) / 3) * 4 + lines * lineSeparator.length();
StringBuilder buf = new StringBuilder(bufLen);
int ip = 0;
while (ip < iLen) {
int l = Math.min(iLen - ip, blockLen);
buf.append(encode(in, iOff + ip, l));
buf.append(lineSeparator);
ip += l;
}
return buf.toString();
}
/**
* Encodes a byte array into Base64 format.
* No blanks or line breaks are inserted in the output.
* @param in An array containing the data bytes to be encoded.
* @return A character array containing the Base64 encoded data.
*/
public static char[] encode(byte[] in) {
return encode(in, 0, in.length);
}
/**
* Encodes a byte array into Base64 format.
* No blanks or line breaks are inserted in the output.
* @param in An array containing the data bytes to be encoded.
* @param iLen Number of bytes to process in <code>in</code>.
* @return A character array containing the Base64 encoded data.
*/
public static char[] encode(byte[] in, int iLen) {
return encode(in, 0, iLen);
}
/**
* Encodes a byte array into Base64 format.
* No blanks or line breaks are inserted in the output.
* @param in An array containing the data bytes to be encoded.
* @param iOff Offset of the first byte in <code>in</code> to be processed.
* @param iLen Number of bytes to process in <code>in</code>, starting at <code>iOff</code>.
* @return A character array containing the Base64 encoded data.
*/
public static char[] encode(byte[] in, int iOff, int iLen) {
int oDataLen = (iLen * 4 + 2) / 3; // output length without padding
int oLen = ((iLen + 2) / 3) * 4; // output length including padding
char[] out = new char[oLen];
int ip = iOff;
int iEnd = iOff + iLen;
int op = 0;
while (ip < iEnd) {
int i0 = in[ip++] & 0xff;
int i1 = ip < iEnd ? in[ip++] & 0xff : 0;
int i2 = ip < iEnd ? in[ip++] & 0xff : 0;
int o0 = i0 >>> 2;
int o1 = ((i0 & 3) << 4) | (i1 >>> 4);
int o2 = ((i1 & 0xf) << 2) | (i2 >>> 6);
int o3 = i2 & 0x3F;
out[op++] = map1[o0];
out[op++] = map1[o1];
out[op] = op < oDataLen ? map1[o2] : '=';
op++;
out[op] = op < oDataLen ? map1[o3] : '=';
op++;
}
return out;
}
/**
* Decodes a string from Base64 format.
* No blanks or line breaks are allowed within the Base64 encoded input data.
* @param s A Base64 String to be decoded.
* @return A String containing the decoded data.
* @throws IllegalArgumentException If the input is not valid Base64 encoded data.
*/
public static String decodeString(String s) {
return new String(decode(s));
}
/**
* Decodes a byte array from Base64 format and ignores line separators, tabs and blanks.
* CR, LF, Tab and Space characters are ignored in the input data.
* This method is compatible with <code>sun.misc.BASE64Decoder.decodeBuffer(String)</code>.
* @param s A Base64 String to be decoded.
* @return An array containing the decoded data bytes.
* @throws IllegalArgumentException If the input is not valid Base64 encoded data.
*/
public static byte[] decodeLines(String s) {
char[] buf = new char[s.length()];
int p = 0;
for (int ip = 0; ip < s.length(); ip++) {
char c = s.charAt(ip);
if (c != ' ' && c != '\r' && c != '\n' && c != '\t') {
buf[p++] = c;
}
}
return decode(buf, 0, p);
}
/**
* Decodes a byte array from Base64 format.
* No blanks or line breaks are allowed within the Base64 encoded input data.
* @param s A Base64 String to be decoded.
* @return An array containing the decoded data bytes.
* @throws IllegalArgumentException If the input is not valid Base64 encoded data.
*/
public static byte[] decode(String s) {
return decode(s.toCharArray());
}
/**
* Decodes a byte array from Base64 format.
* No blanks or line breaks are allowed within the Base64 encoded input data.
* @param in A character array containing the Base64 encoded data.
* @return An array containing the decoded data bytes.
* @throws IllegalArgumentException If the input is not valid Base64 encoded data.
*/
public static byte[] decode(char[] in) {
return decode(in, 0, in.length);
}
/**
* Decodes a byte array from Base64 format.
* No blanks or line breaks are allowed within the Base64 encoded input data.
* @param in A character array containing the Base64 encoded data.
* @param iOff Offset of the first character in <code>in</code> to be processed.
* @param iLen Number of characters to process in <code>in</code>, starting at <code>iOff</code>.
* @return An array containing the decoded data bytes.
* @throws IllegalArgumentException If the input is not valid Base64 encoded data.
*/
public static byte[] decode(char[] in, int iOff, int iLen) {
if (iLen % 4 != 0) {
throw new IllegalArgumentException("Length of Base64 encoded input string is not a multiple of 4.");
}
while (iLen > 0 && in[iOff + iLen - 1] == '=') {
iLen--;
}
int oLen = (iLen * 3) / 4;
byte[] out = new byte[oLen];
int ip = iOff;
int iEnd = iOff + iLen;
int op = 0;
while (ip < iEnd) {
int i0 = in[ip++];
int i1 = in[ip++];
int i2 = ip < iEnd ? in[ip++] : 'A';
int i3 = ip < iEnd ? in[ip++] : 'A';
if (i0 > 127 || i1 > 127 || i2 > 127 || i3 > 127) {
throw new IllegalArgumentException("Illegal character in Base64 encoded data.");
}
int b0 = map2[i0];
int b1 = map2[i1];
int b2 = map2[i2];
int b3 = map2[i3];
if (b0 < 0 || b1 < 0 || b2 < 0 || b3 < 0) {
throw new IllegalArgumentException("Illegal character in Base64 encoded data.");
}
int o0 = (b0 << 2) | (b1 >>> 4);
int o1 = ((b1 & 0xf) << 4) | (b2 >>> 2);
int o2 = ((b2 & 3) << 6) | b3;
out[op++] = (byte) o0;
if (op < oLen) {
out[op++] = (byte) o1;
}
if (op < oLen) {
out[op++] = (byte) o2;
}
}
return out;
}
// Dummy constructor.
private Base64Coder() {
}
} // end class Base64Coder

View File

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

View File

@ -0,0 +1,181 @@
/*
* Copyright (C) 2009 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.envaya.sms;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
import android.util.Log;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import org.envaya.sms.R;
import org.envaya.sms.ui.Main;
/*
* Service running in foreground to make sure App instance stays
* in memory (otherwise we could lose timestamps of sent messages
* which could cause us to exceed Android's SMS sending limit)
*
* Also adds notification to status bar.
*/
public class ForegroundService extends Service {
private App app;
private static final Class<?>[] mSetForegroundSignature = new Class[] {
boolean.class};
private static final Class<?>[] mStartForegroundSignature = new Class[] {
int.class, Notification.class};
private static final Class<?>[] mStopForegroundSignature = new Class[] {
boolean.class};
private NotificationManager mNM;
private Method mSetForeground;
private Method mStartForeground;
private Method mStopForeground;
private Object[] mSetForegroundArgs = new Object[1];
private Object[] mStartForegroundArgs = new Object[2];
private Object[] mStopForegroundArgs = new Object[1];
void invokeMethod(Method method, Object[] args) {
try {
method.invoke(this, args);
} catch (InvocationTargetException e) {
// Should not happen.
Log.w("ApiDemos", "Unable to invoke method", e);
} catch (IllegalAccessException e) {
// Should not happen.
Log.w("ApiDemos", "Unable to invoke method", e);
}
}
/**
* This is a wrapper around the new startForeground method, using the older
* APIs if it is not available.
*/
void startForegroundCompat(int id, Notification notification) {
// If we have the new startForeground API, then use it.
if (mStartForeground != null) {
mStartForegroundArgs[0] = Integer.valueOf(id);
mStartForegroundArgs[1] = notification;
invokeMethod(mStartForeground, mStartForegroundArgs);
return;
}
// Fall back on the old API.
mSetForegroundArgs[0] = Boolean.TRUE;
invokeMethod(mSetForeground, mSetForegroundArgs);
mNM.notify(id, notification);
}
/**
* This is a wrapper around the new stopForeground method, using the older
* APIs if it is not available.
*/
void stopForegroundCompat(int id) {
// If we have the new stopForeground API, then use it.
if (mStopForeground != null) {
mStopForegroundArgs[0] = Boolean.TRUE;
invokeMethod(mStopForeground, mStopForegroundArgs);
return;
}
// Fall back on the old API. Note to cancel BEFORE changing the
// foreground state, since we could be killed at that point.
mNM.cancel(id);
mSetForegroundArgs[0] = Boolean.FALSE;
invokeMethod(mSetForeground, mSetForegroundArgs);
}
@Override
public void onCreate() {
mNM = (NotificationManager)getSystemService(NOTIFICATION_SERVICE);
app = (App)getApplication();
try {
mStartForeground = getClass().getMethod("startForeground",
mStartForegroundSignature);
mStopForeground = getClass().getMethod("stopForeground",
mStopForegroundSignature);
return;
} catch (NoSuchMethodException e) {
// Running on an older platform.
mStartForeground = mStopForeground = null;
}
try {
mSetForeground = getClass().getMethod("setForeground",
mSetForegroundSignature);
} catch (NoSuchMethodException e) {
throw new IllegalStateException(
"OS doesn't have Service.startForeground OR Service.setForeground!");
}
}
@Override
public void onDestroy() {
// Make sure our notification is gone.
stopForegroundCompat(R.string.service_started);
}
// This is the old onStart method that will be called on the pre-2.0
// platform. On 2.0 or later we override onStartCommand() so this
// method will not be called.
@Override
public void onStart(Intent intent, int startId) {
handleCommand(intent);
}
//@Override
public int onStartCommand(Intent intent, int flags, int startId) {
handleCommand(intent);
// We want this service to continue running until it is explicitly
// stopped, so return sticky.
return 1; //START_STICKY;
}
void handleCommand(Intent intent)
{
if (app.isEnabled())
{
CharSequence text = getText(R.string.service_started);
Notification notification = new Notification(R.drawable.icon, text,
System.currentTimeMillis());
PendingIntent contentIntent = PendingIntent.getActivity(this, 0,
new Intent(this, Main.class), 0);
notification.setLatestEventInfo(this,
"EnvayaSMS running",
text, contentIntent);
startForegroundCompat(R.string.service_started, notification);
}
else
{
this.stopForegroundCompat(R.string.service_started);
}
}
@Override
public IBinder onBind(Intent intent) {
return null;
}
}

View File

@ -0,0 +1,69 @@
package org.envaya.sms;
import android.content.Intent;
import android.net.Uri;
import org.envaya.sms.receiver.IncomingMessageRetry;
public abstract class IncomingMessage extends QueuedMessage {
protected String from;
public IncomingMessage(App app, String from)
{
super(app);
this.from = from;
}
public abstract String getDisplayType();
public boolean isForwardable()
{
if (app.isTestMode() && !app.isTestPhoneNumber(from))
{
return false;
}
/*
* Don't forward messages from shortcodes or users with
* addresses like 'Vodacom' because they're likely to be
* messages from network, or spam. At least for network
* messages we should let them go in to the Messaging inbox
* because the person managing this phone needs to know
* when they're out of credit, etc.
*
* The minimum length of normal subscriber numbers doesn't
* seem to be specified, but in practice seems to be
* at least 7 digits everywhere.
*/
int fromDigits = 0;
int fromLength = from.length();
for (int i = 0; i < fromLength; i++)
{
if (Character.isDigit(from.charAt(i)))
{
fromDigits++;
}
}
return fromDigits >= 7;
}
public String getFrom()
{
return from;
}
public void retryNow() {
app.log("Retrying forwarding message from " + from);
tryForwardToServer();
}
protected Intent getRetryIntent() {
Intent intent = new Intent(app, IncomingMessageRetry.class);
intent.setData(this.getUri());
return intent;
}
public abstract void tryForwardToServer();
}

View File

@ -0,0 +1,165 @@
package org.envaya.sms;
import android.net.Uri;
import java.io.IOException;
import java.util.ArrayList;
import org.json.*;
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 {
List<MmsPart> parts;
long id;
String contentLocation;
public IncomingMms(App app, String from, long id)
{
super(app, from);
this.parts = new ArrayList<MmsPart>();
this.id = id;
}
public String getDisplayType()
{
return "MMS";
}
public List<MmsPart> getParts()
{
return parts;
}
public void addPart(MmsPart part)
{
parts.add(part);
}
public long getId()
{
return id;
}
public String getContentLocation()
{
return contentLocation;
}
public void setContentLocation(String contentLocation)
{
this.contentLocation = contentLocation;
}
@Override
public String toString()
{
StringBuilder builder = new StringBuilder();
builder.append("MMS id=");
builder.append(id);
builder.append(" from=");
builder.append(from);
builder.append(":\n");
for (MmsPart part : parts)
{
builder.append(" ");
builder.append(part.toString());
builder.append("\n");
}
return builder.toString();
}
public void tryForwardToServer()
{
app.log("Forwarding MMS to server...");
List<FormBodyPart> formParts = new ArrayList<FormBodyPart>();
int i = 0;
String message = "";
JSONArray partsMetadata = new JSONArray();
for (MmsPart part : parts)
{
String formFieldName = "part" + i;
String text = part.getText();
String contentType = part.getContentType();
String partName = part.getName();
if ("text/plain".equals(contentType))
{
message = text;
}
ContentBody body;
if (text != null)
{
if (contentType != null)
{
contentType += "; charset=UTF-8";
}
body = new ByteArrayBody(text.getBytes(), contentType, partName);
}
else
{
// avoid using InputStreamBody because it forces the HTTP request
// to be sent using Transfer-Encoding: chunked, which is not
// supported by some web servers (including nginx)
try
{
body = new ByteArrayBody(part.getData(), contentType, partName);
}
catch (IOException ex)
{
app.logError("Error reading data for " + part.toString(), ex);
continue;
}
}
try
{
JSONObject partMetadata = new JSONObject();
partMetadata.put("name", formFieldName);
partMetadata.put("cid", part.getContentId());
partMetadata.put("type", part.getContentType());
partMetadata.put("filename", part.getName());
partsMetadata.put(partMetadata);
}
catch (JSONException ex)
{
app.logError("Error encoding MMS part metadata for " + part.toString(), ex);
continue;
}
formParts.add(new FormBodyPart(formFieldName, body));
i++;
}
ForwarderTask task = new ForwarderTask(this,
new BasicNameValuePair("from", getFrom()),
new BasicNameValuePair("message", message),
new BasicNameValuePair("message_type", App.MESSAGE_TYPE_MMS),
new BasicNameValuePair("mms_parts", partsMetadata.toString())
);
task.setFormParts(formParts);
task.execute();
}
public Uri getUri()
{
return Uri.withAppendedPath(App.INCOMING_URI, "mms/" + id);
}
}

View File

@ -0,0 +1,56 @@
package org.envaya.sms;
import android.net.Uri;
import android.telephony.SmsMessage;
import org.apache.http.message.BasicNameValuePair;
import org.envaya.sms.task.ForwarderTask;
public class IncomingSms extends IncomingMessage {
protected String message;
protected long timestampMillis;
// constructor for SMS retrieved from android.provider.Telephony.SMS_RECEIVED intent
public IncomingSms(App app, SmsMessage sms) {
super(app, sms.getOriginatingAddress());
this.message = sms.getMessageBody();
this.timestampMillis = sms.getTimestampMillis();
}
// constructor for SMS retrieved from Messaging inbox
public IncomingSms(App app, String from, String message, long timestampMillis) {
super(app, from);
this.message = message;
this.timestampMillis = timestampMillis;
}
public String getMessageBody()
{
return message;
}
public String getDisplayType()
{
return "SMS";
}
public Uri getUri()
{
return Uri.withAppendedPath(App.INCOMING_URI,
"sms/" +
Uri.encode(from) + "/"
+ timestampMillis + "/" +
Uri.encode(message));
}
public void tryForwardToServer() {
new ForwarderTask(this,
new BasicNameValuePair("from", getFrom()),
new BasicNameValuePair("message_type", App.MESSAGE_TYPE_SMS),
new BasicNameValuePair("message", getMessageBody())
).execute();
}
}

View File

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

170
src/org/envaya/sms/MmsPart.java Executable file
View File

@ -0,0 +1,170 @@
package org.envaya.sms;
import android.net.Uri;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
public class MmsPart {
private App app;
private long partId;
private String contentType;
private String name;
private String text;
private String cid;
private String dataFile;
public MmsPart(App app, long partId)
{
this.app = app;
this.partId = partId;
}
/*
* The part id is the local value of the _id column in the MMS part table
* (see android.provider.Telephony.Part)
*/
public long getPartId()
{
return partId;
}
/*
* The content id of a MMS part is used to resolve references in SMIL
* like <img region="Image" src="cid:805"/> . Telephony.java claims
* that the cid column is an integer, but it is actually a string
* like "<0000>" or "<83>".
*/
public void setContentId(String cid)
{
this.cid = cid;
}
public String getContentId()
{
return cid;
}
/*
* Common Content-Type values for MMS parts include:
* application/smil
* text/plain
* image/jpeg
*/
public void setContentType(String contentType)
{
this.contentType = contentType;
}
public String getContentType()
{
return contentType;
}
/*
* The name of an MMS part is the filename of the original file sent
* (e.g. Image001.jpg). For text/SMIL parts, the filename is generated by the
* sending phone and can usually be ignored.
*/
public void setName(String name)
{
this.name = name;
}
public String getName()
{
return name;
}
/*
* The text is the content of text-based MMS parts (application/smil,
* text/plain, or text/html), and is null for multimedia parts.
*/
public void setText(String text)
{
this.text = text;
}
public String getText()
{
return text;
}
public void setDataFile(String dataFile)
{
this.dataFile = dataFile;
}
public String getDataFile()
{
return dataFile;
}
public long getDataLength()
{
if (dataFile != null)
{
return new File(dataFile).length();
}
else
{
return -1;
}
}
public byte[] getData() throws IOException
{
int length = (int)getDataLength();
byte[] bytes = new byte[length];
int offset = 0;
int bytesRead = 0;
InputStream in = openInputStream();
while (offset < bytes.length)
{
bytesRead = in.read(bytes, offset, bytes.length - offset);
if (bytesRead < 0)
{
break;
}
offset += bytesRead;
}
in.close();
if (offset < bytes.length)
{
throw new IOException("Failed to read complete data of MMS part");
}
return bytes;
}
/*
* For multimedia parts, the _data column of the MMS Parts table contains the
* path on the Android filesystem containing that file, and openInputStream
* returns an InputStream for this file.
*/
public InputStream openInputStream() throws FileNotFoundException
{
return app.getContentResolver().openInputStream(getContentUri());
}
@Override
public String toString()
{
return "part " + partId + ": " + contentType + "; name=" + name + "; cid=" + cid;
}
public Uri getContentUri()
{
return Uri.parse("content://mms/part/" + partId);
}
}

170
src/org/envaya/sms/MmsUtils.java Executable file
View File

@ -0,0 +1,170 @@
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;
/*
* Utilities for parsing IncomingMms from the MMS content provider tables,
* as defined by android.provider.Telephony
*
* Analogous to com.google.android.mms.pdu.PduPersister from
* core/java/com/google/android/mms/pdu in the base Android framework
* (https://github.com/android/platform_frameworks_base)
*/
public class MmsUtils
{
// constants from android.provider.Telephony
public static final Uri OBSERVER_URI = Uri.parse("content://mms-sms/");
public static final Uri INBOX_URI = Uri.parse("content://mms/inbox");
public static final Uri PART_URI = Uri.parse("content://mms/part");
// constants from com.google.android.mms.pdu.PduHeaders
private static final int PDU_HEADER_FROM = 0x89;
private static final int MESSAGE_TYPE_RETRIEVE_CONF = 0x84;
// todo -- prevent unbounded growth?
private final Set<Long> seenMmsIds = new HashSet<Long>();
private App app;
private ContentResolver contentResolver;
public MmsUtils(App app)
{
this.app = app;
this.contentResolver = app.getContentResolver();
}
private List<MmsPart> getMmsParts(long id)
{
Cursor cur = contentResolver.query(PART_URI, new String[] {
"_id", "ct", "name", "text", "cid", "_data"
}, "mid = ?", new String[] { "" + id }, null);
// assume that if there is at least one part saved in database
// then MMS is fully delivered (this seems to be true in practice)
List<MmsPart> parts = new ArrayList<MmsPart>();
while (cur.moveToNext())
{
long partId = cur.getLong(0);
MmsPart part = new MmsPart(app, partId);
part.setContentType(cur.getString(1));
part.setName(cur.getString(2));
part.setDataFile(cur.getString(5));
// todo interpret charset like com.google.android.mms.pdu.EncodedStringValue
part.setText(cur.getString(3));
part.setContentId(cur.getString(4));
parts.add(part);
}
cur.close();
return parts;
}
/*
* see com.google.android.mms.pdu.PduPersister.loadAddress
*/
private String getSenderNumber(long mmsId) {
Uri uri = Uri.parse("content://mms/"+mmsId+"/addr");
Cursor cur = contentResolver.query(uri,
new String[] { "address", "charset", "type" },
null, null, null);
String address = null;
while (cur.moveToNext())
{
int addrType = cur.getInt(2);
if (addrType == PDU_HEADER_FROM)
{
// todo interpret charset like com.google.android.mms.pdu.EncodedStringValue
address = cur.getString(0);
}
}
cur.close();
return address;
}
public List<IncomingMms> getMessagesInInbox()
{
// the M-Retrieve.conf messages are the 'actual' MMS messages
String m_type = "" + MESSAGE_TYPE_RETRIEVE_CONF;
Cursor c = contentResolver.query(INBOX_URI,
new String[] {"_id", "ct_l"},
"m_type = ? ", new String[] { m_type }, null);
List<IncomingMms> messages = new ArrayList<IncomingMms>();
while (c.moveToNext())
{
long id = c.getLong(0);
IncomingMms mms = new IncomingMms(app, getSenderNumber(id), id);
mms.setContentLocation(c.getString(1));
for (MmsPart part : getMmsParts(id))
{
mms.addPart(part);
}
messages.add(mms);
}
c.close();
return messages;
}
public synchronized boolean deleteFromInbox(IncomingMms mms)
{
long id = mms.getId();
Uri uri = Uri.parse("content://mms/inbox/" + id);
int res = contentResolver.delete(uri, null, null);
if (res > 0)
{
app.log("MMS id="+id+" deleted from inbox");
// remove id from set because Messaging app reuses ids
// of deleted messages.
// TODO: handle reuse of IDs deleted directly through Messaging
// app while EnvayaSMS is running
seenMmsIds.remove(id);
}
else
{
app.log("MMS id="+id+" could not be deleted from inbox");
}
return res > 0;
}
public synchronized void markOldMms(IncomingMms mms)
{
long id = mms.getId();
seenMmsIds.add(id);
}
public synchronized boolean isNewMms(IncomingMms mms)
{
long id = mms.getId();
return !seenMmsIds.contains(id);
}
}

View File

@ -0,0 +1,107 @@
package org.envaya.sms;
import org.envaya.sms.receiver.OutgoingMessageRetry;
import android.content.Intent;
import android.net.Uri;
public class OutgoingMessage extends QueuedMessage {
private String serverId;
private String message;
private String from;
private String to;
private String localId;
private static int nextLocalId = 1;
public OutgoingMessage(App app)
{
super(app);
this.localId = "_o" + getNextLocalId();
}
static synchronized int getNextLocalId()
{
return nextLocalId++;
}
public Uri getUri()
{
return Uri.withAppendedPath(App.OUTGOING_URI, ((serverId == null) ? localId : serverId));
}
public String getLogName()
{
return (serverId == null) ? "SMS reply" : ("SMS id=" + serverId);
}
public String getServerId()
{
return serverId;
}
public void setServerId(String id)
{
this.serverId = id;
}
public String getMessageBody()
{
return message;
}
public void setMessageBody(String message)
{
this.message = message;
}
public String getFrom()
{
return from;
}
public void setFrom(String from)
{
this.from = from;
}
public String getTo()
{
return to;
}
public void setTo(String to)
{
this.to = to;
}
public void retryNow() {
app.log("Retrying sending " + getLogName() + " to " + getTo());
trySend();
}
public void trySend()
{
String packageName = app.chooseOutgoingSmsPackage();
if (packageName == null)
{
// todo... schedule retry
return;
}
Intent intent = new Intent(packageName + App.OUTGOING_SMS_INTENT_SUFFIX, this.getUri());
intent.putExtra(App.OUTGOING_SMS_EXTRA_TO, getTo());
intent.putExtra(App.OUTGOING_SMS_EXTRA_BODY, getMessageBody());
app.sendBroadcast(intent, "android.permission.SEND_SMS");
}
protected Intent getRetryIntent() {
Intent intent = new Intent(app, OutgoingMessageRetry.class);
intent.setData(this.getUri());
return intent;
}
}

View File

@ -0,0 +1,72 @@
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.os.SystemClock;
public abstract class QueuedMessage
{
protected long nextRetryTime = 0;
protected int numRetries = 0;
public App app;
public QueuedMessage(App app)
{
this.app = app;
}
public boolean canRetryNow() {
return (nextRetryTime > 0 && nextRetryTime < SystemClock.elapsedRealtime());
}
public boolean scheduleRetry() {
long now = SystemClock.elapsedRealtime();
numRetries++;
if (numRetries > 4) {
app.log("5th failure: giving up");
return false;
}
int second = 1000;
int minute = second * 60;
if (numRetries == 1) {
app.log("1st failure; retry in 1 minute");
nextRetryTime = now + 1 * minute;
} else if (numRetries == 2) {
app.log("2nd failure; retry in 10 minutes");
nextRetryTime = now + 10 * minute;
} else if (numRetries == 3) {
app.log("3rd failure; retry in 1 hour");
nextRetryTime = now + 60 * minute;
} else {
app.log("4th failure: retry in 1 day");
nextRetryTime = now + 24 * 60 * minute;
}
AlarmManager alarm = (AlarmManager) app.getSystemService(Context.ALARM_SERVICE);
PendingIntent pendingIntent = PendingIntent.getBroadcast(app,
0,
getRetryIntent(),
0);
alarm.set(
AlarmManager.ELAPSED_REALTIME_WAKEUP,
nextRetryTime,
pendingIntent);
return true;
}
public abstract Uri getUri();
public abstract void retryNow();
protected abstract Intent getRetryIntent();
}

View File

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

View File

@ -0,0 +1,22 @@
package org.envaya.sms.receiver;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import org.envaya.sms.App;
public class ExpansionPackInstallReceiver extends BroadcastReceiver
{
@Override
public void onReceive(Context context, Intent intent)
{
App app = (App) context.getApplicationContext();
String packageName = intent.getData().getSchemeSpecificPart();
if (packageName != null && packageName.startsWith(context.getPackageName() + ".pack"))
{
app.updateExpansionPacks();
}
}
}

View File

@ -0,0 +1,17 @@
package org.envaya.sms.receiver;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import org.envaya.sms.App;
public class IncomingMessageRetry extends BroadcastReceiver
{
@Override
public void onReceive(Context context, Intent intent)
{
App app = (App) context.getApplicationContext();
app.retryIncomingMessage(intent.getData());
}
}

View File

@ -0,0 +1,32 @@
/*
* To change this template, choose Tools | Templates
* and open the template in the editor.
*/
package org.envaya.sms.receiver;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import org.envaya.sms.App;
public class MessageStatusNotifier extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
App app = (App) context.getApplicationContext();
Uri uri = intent.getData();
int resultCode = getResultCode();
// uncomment to test retry on outgoing message failure
/*
if (Math.random() > 0.4)
{
resultCode = SmsManager.RESULT_ERROR_NO_SERVICE;
}
*/
app.notifyOutgoingMessageStatus(uri, resultCode);
}
}

View File

@ -0,0 +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();
}
}

View File

@ -0,0 +1,17 @@
package org.envaya.sms.receiver;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import org.envaya.sms.App;
public class OutgoingMessageRetry extends BroadcastReceiver
{
@Override
public void onReceive(Context context, Intent intent)
{
App app = (App) context.getApplicationContext();
app.retryOutgoingMessage(intent.getData());
}
}

View File

@ -0,0 +1,31 @@
package org.envaya.sms.receiver;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.telephony.SmsManager;
import org.envaya.sms.App;
public class OutgoingSmsReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent)
{
Bundle extras = intent.getExtras();
String to = extras.getString(App.OUTGOING_SMS_EXTRA_TO);
String body = extras.getString(App.OUTGOING_SMS_EXTRA_BODY);
SmsManager smgr = SmsManager.getDefault();
Intent statusIntent = new Intent(App.MESSAGE_STATUS_INTENT, intent.getData());
PendingIntent sentIntent = PendingIntent.getBroadcast(
context,
0,
statusIntent,
PendingIntent.FLAG_ONE_SHOT);
smgr.sendTextMessage(to, null, body, sentIntent, null);
}
}

View File

@ -0,0 +1,68 @@
package org.envaya.sms.receiver;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.telephony.SmsMessage;
import java.util.ArrayList;
import java.util.List;
import org.envaya.sms.App;
import org.envaya.sms.IncomingMessage;
import org.envaya.sms.IncomingSms;
public class SmsReceiver extends BroadcastReceiver {
private App app;
@Override
// source: http://www.devx.com/wireless/Article/39495/1954
public void onReceive(Context context, Intent intent) {
app = (App) context.getApplicationContext();
if (!app.isEnabled())
{
return;
}
try {
boolean hasUnhandledMessage = false;
for (IncomingMessage sms : getMessagesFromIntent(intent)) {
if (sms.isForwardable())
{
app.forwardToServer(sms);
}
else
{
app.log("Ignoring incoming SMS from " + sms.getFrom());
hasUnhandledMessage = true;
}
}
if (!hasUnhandledMessage && !app.getKeepInInbox())
{
this.abortBroadcast();
}
} catch (Throwable ex) {
app.logError("Unexpected error in SmsReceiver", ex, true);
}
}
// from http://github.com/dimagi/rapidandroid
// source: http://www.devx.com/wireless/Article/39495/1954
private List<IncomingMessage> getMessagesFromIntent(Intent intent)
{
Bundle bundle = intent.getExtras();
List<IncomingMessage> messages = new ArrayList<IncomingMessage>();
for (Object pdu : (Object[]) bundle.get("pdus"))
{
SmsMessage sms = SmsMessage.createFromPdu((byte[]) pdu);
messages.add(new IncomingSms(app, sms));
}
return messages;
}
}

View File

@ -0,0 +1,39 @@
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;
public class ForwarderTask extends HttpTask {
private IncomingMessage originalSms;
public ForwarderTask(IncomingMessage originalSms, BasicNameValuePair... paramsArr) {
super(originalSms.app, paramsArr);
this.originalSms = originalSms;
params.add(new BasicNameValuePair("action", App.ACTION_INCOMING));
}
@Override
protected String getDefaultToAddress() {
return originalSms.getFrom();
}
@Override
protected void handleResponse(HttpResponse response) throws Exception {
for (OutgoingMessage reply : parseResponseXML(response)) {
app.sendOutgoingMessage(reply);
}
app.setIncomingMessageStatus(originalSms, true);
}
@Override
protected void handleFailure() {
app.setIncomingMessageStatus(originalSms, false);
}
}

View File

@ -0,0 +1,242 @@
/*
* To change this template, choose Tools | Templates
* and open the template in the editor.
*/
package org.envaya.sms.task;
import android.os.AsyncTask;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
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.HttpVersion;
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.impl.client.DefaultHttpClient;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.params.HttpParams;
import org.apache.http.params.HttpProtocolParams;
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.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
public class HttpTask extends AsyncTask<String, Void, HttpResponse> {
protected App app;
protected String url;
protected List<BasicNameValuePair> params = new ArrayList<BasicNameValuePair>();
private List<FormBodyPart> formParts;
private boolean useMultipartPost = false;
private HttpPost post;
public HttpTask(App app, BasicNameValuePair... paramsArr)
{
super();
this.app = app;
this.url = app.getServerUrl();
params = new ArrayList<BasicNameValuePair>(Arrays.asList(paramsArr));
params.add(new BasicNameValuePair("version", "" + app.getPackageInfo().versionCode));
params.add(new BasicNameValuePair("phone_number", app.getPhoneNumber()));
}
public void setFormParts(List<FormBodyPart> formParts)
{
useMultipartPost = true;
this.formParts = formParts;
}
private String getSignature()
throws NoSuchAlgorithmException, UnsupportedEncodingException
{
Collections.sort(params, new Comparator() {
public int compare(Object o1, Object o2)
{
return ((BasicNameValuePair)o1).getName().compareTo(((BasicNameValuePair)o2).getName());
}
});
StringBuilder builder = new StringBuilder();
builder.append(url);
for (BasicNameValuePair param : params)
{
builder.append(",");
builder.append(param.getName());
builder.append("=");
builder.append(param.getValue());
}
builder.append(",");
builder.append(app.getPassword());
String value = builder.toString();
MessageDigest md = MessageDigest.getInstance("SHA-1");
md.update(value.getBytes("utf-8"));
byte[] digest = md.digest();
return new String(Base64Coder.encode(digest));
}
protected HttpResponse doInBackground(String... ignored) {
if (url.length() == 0) {
app.log("Can't contact server; Server URL not set");
return null;
}
post = new HttpPost(url);
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));
}
HttpClient client = app.getHttpClient();
String signature = getSignature();
post.setHeader("X-Request-Signature", signature);
HttpResponse response = client.execute(post);
int statusCode = response.getStatusLine().getStatusCode();
if (statusCode == 200)
{
return response;
}
else if (statusCode == 403)
{
response.getEntity().consumeContent();
app.log("Failed to authenticate to server");
app.log("(Phone number or password may be incorrect)");
return null;
}
else
{
response.getEntity().consumeContent();
app.log("Received HTTP " + statusCode + " from server");
return null;
}
}
catch (IOException ex)
{
post.abort();
app.logError("Error while contacting server", ex);
return null;
}
catch (Throwable ex)
{
post.abort();
app.logError("Unexpected error while contacting server", ex, true);
return null;
}
}
protected String getDefaultToAddress()
{
return "";
}
protected List<OutgoingMessage> parseResponseXML(HttpResponse response)
throws IOException, ParserConfigurationException, SAXException
{
List<OutgoingMessage> messages = new ArrayList<OutgoingMessage>();
InputStream responseStream = response.getEntity().getContent();
DocumentBuilder xmlBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
Document xml = xmlBuilder.parse(responseStream);
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);
Node firstChild = smsElement.getFirstChild();
sms.setMessageBody(firstChild != null ? firstChild.getNodeValue(): "");
messages.add(sms);
}
return messages;
}
@Override
protected void onPostExecute(HttpResponse response) {
if (response != null)
{
try
{
handleResponse(response);
response.getEntity().consumeContent();
}
catch (Throwable ex)
{
post.abort();
app.logError("Error processing server response", ex);
handleFailure();
}
}
else
{
handleFailure();
}
}
protected void handleResponse(HttpResponse response) throws Exception
{
}
protected void handleFailure()
{
}
}

View File

@ -0,0 +1,21 @@
package org.envaya.sms.task;
import org.apache.http.HttpResponse;
import org.apache.http.message.BasicNameValuePair;
import org.envaya.sms.App;
import org.envaya.sms.OutgoingMessage;
public class PollerTask extends HttpTask {
public PollerTask(App app) {
super(app, new BasicNameValuePair("action", App.ACTION_OUTGOING));
}
@Override
protected void handleResponse(HttpResponse response) throws Exception {
for (OutgoingMessage reply : parseResponseXML(response)) {
app.sendOutgoingMessage(reply);
}
}
}

View File

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

@ -0,0 +1,86 @@
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();
}
}

35
src/org/envaya/sms/ui/Help.java Executable file
View File

@ -0,0 +1,35 @@
package org.envaya.sms.ui;
import android.app.Activity;
import android.os.Bundle;
import android.text.Html;
import android.widget.TextView;
import org.envaya.sms.App;
import org.envaya.sms.R;
public class Help extends Activity {
@Override
public void onCreate(Bundle icicle) {
super.onCreate(icicle);
setContentView(R.layout.help);
TextView help = (TextView) this.findViewById(R.id.help);
App app = (App)getApplication();
String html = "<b>EnvayaSMS " + app.getPackageInfo().versionName + "</b><br /><br />"
+ "EnvayaSMS is a SMS gateway.<br /><br /> "
+ "It forwards all incoming SMS messages received by this phone to a server on the internet, "
+ "and also sends outgoing SMS messages from that server to other phones.<br /><br />"
+ "(See https://kalsms.net for more information.)<br /><br />"
+ "The Settings screen allows you configure EnvayaSMS to work with a particular server, "
+ "by entering the server URL, your phone number, "
+ "and the password assigned to your phone on the server.<br /><br />"
+ "Menu icons cc/by www.androidicons.com<br /><br />";
help.setText(Html.fromHtml(html));
}
}

View File

@ -0,0 +1,72 @@
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;
}
}

129
src/org/envaya/sms/ui/Main.java Executable file
View File

@ -0,0 +1,129 @@
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 java.util.List;
import org.apache.http.HttpResponse;
import org.apache.http.message.BasicNameValuePair;
import org.envaya.sms.App;
import org.envaya.sms.IncomingMms;
import org.envaya.sms.MmsUtils;
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 class TestTask extends HttpTask
{
public TestTask() {
super(Main.this.app, new BasicNameValuePair("action", App.ACTION_OUTGOING));
}
@Override
protected void handleResponse(HttpResponse response) throws Exception
{
parseResponseXML(response);
app.log("Server connection OK!");
}
}
public void updateLogView()
{
final ScrollView scrollView = (ScrollView) this.findViewById(R.id.info_scroll);
TextView info = (TextView) this.findViewById(R.id.info);
info.setText(app.getDisplayedLog());
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);
TextView info = (TextView) this.findViewById(R.id.info);
info.setMovementMethod(new ScrollingMovementMethod());
updateLogView();
IntentFilter logReceiverFilter = new IntentFilter();
logReceiverFilter.addAction(App.LOG_INTENT);
registerReceiver(logReceiver, logReceiverFilter);
}
@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, ForwardInbox.class));
return true;
case R.id.help:
startActivity(new Intent(this, Help.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 item = menu.findItem(R.id.retry_now);
int stuckMessages = app.getStuckMessageCount();
item.setEnabled(stuckMessages > 0);
item.setTitle("Retry Fwd (" + stuckMessages + ")");
return true;
}
}

167
src/org/envaya/sms/ui/Prefs.java Executable file
View File

@ -0,0 +1,167 @@
package org.envaya.sms.ui;
import android.content.SharedPreferences;
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
import android.os.Bundle;
import android.preference.EditTextPreference;
import android.preference.ListPreference;
import android.preference.Preference;
import android.preference.PreferenceActivity;
import android.preference.PreferenceScreen;
import android.provider.Settings;
import android.provider.Settings.SettingNotFoundException;
import android.view.Menu;
import org.envaya.sms.App;
import org.envaya.sms.R;
public class Prefs extends PreferenceActivity implements OnSharedPreferenceChangeListener {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
addPreferencesFromResource(R.xml.prefs);
PreferenceScreen screen = this.getPreferenceScreen();
int numPrefs = screen.getPreferenceCount();
for(int i=0; i < numPrefs;i++)
{
updatePrefSummary(screen.getPreference(i));
}
}
@Override
protected void onResume(){
super.onResume();
// Set up a listener whenever a key changes
getPreferenceScreen().getSharedPreferences().registerOnSharedPreferenceChangeListener(this);
}
@Override
protected void onPause() {
super.onPause();
// Unregister the listener whenever a key changes
getPreferenceScreen().getSharedPreferences().unregisterOnSharedPreferenceChangeListener(this);
}
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
App app = (App) getApplication();
if (key.equals("outgoing_interval"))
{
app.setOutgoingMessageAlarm();
}
else if (key.equals("wifi_sleep_policy"))
{
int value;
String valueStr = sharedPreferences.getString("wifi_sleep_policy", "screen");
if ("screen".equals(valueStr))
{
value = Settings.System.WIFI_SLEEP_POLICY_DEFAULT;
}
else if ("plugged".equals(valueStr))
{
value = Settings.System.WIFI_SLEEP_POLICY_NEVER_WHILE_PLUGGED;
}
else
{
value = Settings.System.WIFI_SLEEP_POLICY_NEVER;
}
Settings.System.putInt(getContentResolver(),
Settings.System.WIFI_SLEEP_POLICY, value);
}
else if (key.equals("server_url"))
{
String serverUrl = sharedPreferences.getString("server_url", "");
// assume http:// scheme if none entered
if (serverUrl.length() > 0 && !serverUrl.contains("://"))
{
sharedPreferences.edit()
.putString("server_url", "http://" + serverUrl)
.commit();
}
app.log("Server URL changed to: " + app.getDisplayString(app.getServerUrl()));
}
else if (key.equals("phone_number"))
{
app.log("Phone number changed to: " + app.getDisplayString(app.getPhoneNumber()));
}
else if (key.equals("test_mode"))
{
app.log("Test mode changed to: " + (app.isTestMode() ? "ON": "OFF"));
}
else if (key.equals("password"))
{
app.log("Password changed");
}
else if (key.equals("enabled"))
{
app.log(app.isEnabled() ? "SMS Gateway started." : "SMS Gateway stopped.");
app.enabledChanged();
}
updatePrefSummary(findPreference(key));
}
private void updatePrefSummary(Preference p)
{
if ("wifi_sleep_policy".equals(p.getKey()))
{
int sleepPolicy;
try
{
sleepPolicy = Settings.System.getInt(this.getContentResolver(),
Settings.System.WIFI_SLEEP_POLICY);
}
catch (SettingNotFoundException ex)
{
sleepPolicy = Settings.System.WIFI_SLEEP_POLICY_DEFAULT;
}
switch (sleepPolicy)
{
case Settings.System.WIFI_SLEEP_POLICY_DEFAULT:
p.setSummary("Wi-Fi will disconnect when the phone sleeps");
break;
case Settings.System.WIFI_SLEEP_POLICY_NEVER_WHILE_PLUGGED:
p.setSummary("Wi-Fi will disconnect when the phone sleeps unless it is plugged in");
break;
case Settings.System.WIFI_SLEEP_POLICY_NEVER:
p.setSummary("Wi-Fi will stay connected when the phone sleeps");
break;
}
}
else if (p instanceof ListPreference) {
p.setSummary(((ListPreference)p).getEntry());
}
else if (p instanceof EditTextPreference) {
EditTextPreference textPref = (EditTextPreference)p;
String text = textPref.getText();
if (text == null || text.equals(""))
{
p.setSummary("(not set)");
}
else if (p.getKey().equals("password"))
{
p.setSummary("********");
}
else
{
p.setSummary(text);
}
}
}
// any other time the Menu key is pressed
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
this.finish();
return (true);
}
}

View File

@ -0,0 +1,107 @@
package org.envaya.sms.ui;
import android.app.AlertDialog;
import android.app.ListActivity;
import android.content.DialogInterface;
import android.content.DialogInterface.OnClickListener;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.ArrayAdapter;
import android.widget.EditText;
import android.widget.ListView;
import android.widget.TextView;
import org.envaya.sms.App;
import org.envaya.sms.R;
public class TestPhoneNumbers extends ListActivity {
private App app;
@Override
public void onCreate(Bundle icicle) {
super.onCreate(icicle);
setContentView(R.layout.test_phone_numbers);
app = (App)getApplication();
ListView lv = getListView();
lv.setOnItemClickListener(new OnItemClickListener() {
public void onItemClick(AdapterView<?> parent, View view,
int position, long id)
{
final String phoneNumber = ((TextView) view).getText().toString();
new AlertDialog.Builder(TestPhoneNumbers.this)
.setTitle("Remove Test Phone")
.setMessage("Do you want to remove "+phoneNumber
+" from the list of test phone numbers?")
.setPositiveButton("OK",
new OnClickListener() {
public void onClick(DialogInterface dialog, int which)
{
app.removeTestPhoneNumber(phoneNumber);
updateTestPhoneNumbers();
dialog.dismiss();
}
}
)
.setNegativeButton("Cancel",
new OnClickListener() {
public void onClick(DialogInterface dialog, int which)
{
dialog.dismiss();
}
}
)
.show();
}
});
updateTestPhoneNumbers();
}
public void updateTestPhoneNumbers()
{
String[] senders = app.getTestPhoneNumbers().toArray(new String[]{});
ArrayAdapter<String> arrayAdapter = new ArrayAdapter<String>(this,
R.layout.test_phone_number,
senders);
setListAdapter(arrayAdapter);
}
public void addTestSender(View v)
{
LayoutInflater factory = LayoutInflater.from(this);
final EditText textEntryView =
(EditText)factory.inflate(R.layout.add_test_phone_number, null);
new AlertDialog.Builder(this)
.setTitle("Add Test Phone")
.setMessage("Enter the phone number that you will be testing with:")
.setView(textEntryView)
.setPositiveButton("OK",
new OnClickListener() {
public void onClick(DialogInterface dialog, int which)
{
app.addTestPhoneNumber(textEntryView.getText().toString());
updateTestPhoneNumbers();
dialog.dismiss();
}
}
)
.setNegativeButton("Cancel",
new OnClickListener() {
public void onClick(DialogInterface dialog, int which)
{
dialog.dismiss();
}
}
)
.show();
}
}