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:
692
src/org/envaya/sms/App.java
Executable file
692
src/org/envaya/sms/App.java
Executable 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;
|
||||
}
|
||||
}
|
269
src/org/envaya/sms/Base64Coder.java
Executable file
269
src/org/envaya/sms/Base64Coder.java
Executable 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
|
57
src/org/envaya/sms/CheckMmsInboxService.java
Executable file
57
src/org/envaya/sms/CheckMmsInboxService.java
Executable 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
181
src/org/envaya/sms/ForegroundService.java
Executable file
181
src/org/envaya/sms/ForegroundService.java
Executable 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;
|
||||
}
|
||||
}
|
69
src/org/envaya/sms/IncomingMessage.java
Executable file
69
src/org/envaya/sms/IncomingMessage.java
Executable 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();
|
||||
}
|
165
src/org/envaya/sms/IncomingMms.java
Executable file
165
src/org/envaya/sms/IncomingMms.java
Executable 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);
|
||||
}
|
||||
}
|
56
src/org/envaya/sms/IncomingSms.java
Executable file
56
src/org/envaya/sms/IncomingSms.java
Executable 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();
|
||||
}
|
||||
|
||||
}
|
46
src/org/envaya/sms/MmsObserver.java
Executable file
46
src/org/envaya/sms/MmsObserver.java
Executable 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
170
src/org/envaya/sms/MmsPart.java
Executable 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
170
src/org/envaya/sms/MmsUtils.java
Executable 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);
|
||||
}
|
||||
}
|
107
src/org/envaya/sms/OutgoingMessage.java
Executable file
107
src/org/envaya/sms/OutgoingMessage.java
Executable 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;
|
||||
}
|
||||
}
|
72
src/org/envaya/sms/QueuedMessage.java
Executable file
72
src/org/envaya/sms/QueuedMessage.java
Executable 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();
|
||||
}
|
15
src/org/envaya/sms/receiver/BootReceiver.java
Executable file
15
src/org/envaya/sms/receiver/BootReceiver.java
Executable 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
|
||||
}
|
||||
}
|
22
src/org/envaya/sms/receiver/ExpansionPackInstallReceiver.java
Executable file
22
src/org/envaya/sms/receiver/ExpansionPackInstallReceiver.java
Executable 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();
|
||||
}
|
||||
}
|
||||
}
|
17
src/org/envaya/sms/receiver/IncomingMessageRetry.java
Executable file
17
src/org/envaya/sms/receiver/IncomingMessageRetry.java
Executable 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());
|
||||
}
|
||||
}
|
32
src/org/envaya/sms/receiver/MessageStatusNotifier.java
Executable file
32
src/org/envaya/sms/receiver/MessageStatusNotifier.java
Executable 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);
|
||||
}
|
||||
}
|
15
src/org/envaya/sms/receiver/OutgoingMessagePoller.java
Executable file
15
src/org/envaya/sms/receiver/OutgoingMessagePoller.java
Executable 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();
|
||||
}
|
||||
}
|
17
src/org/envaya/sms/receiver/OutgoingMessageRetry.java
Executable file
17
src/org/envaya/sms/receiver/OutgoingMessageRetry.java
Executable 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());
|
||||
}
|
||||
}
|
31
src/org/envaya/sms/receiver/OutgoingSmsReceiver.java
Executable file
31
src/org/envaya/sms/receiver/OutgoingSmsReceiver.java
Executable 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);
|
||||
}
|
||||
}
|
68
src/org/envaya/sms/receiver/SmsReceiver.java
Executable file
68
src/org/envaya/sms/receiver/SmsReceiver.java
Executable 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;
|
||||
}
|
||||
}
|
39
src/org/envaya/sms/task/ForwarderTask.java
Executable file
39
src/org/envaya/sms/task/ForwarderTask.java
Executable 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);
|
||||
}
|
||||
}
|
242
src/org/envaya/sms/task/HttpTask.java
Executable file
242
src/org/envaya/sms/task/HttpTask.java
Executable 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()
|
||||
{
|
||||
}
|
||||
}
|
21
src/org/envaya/sms/task/PollerTask.java
Executable file
21
src/org/envaya/sms/task/PollerTask.java
Executable 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);
|
||||
}
|
||||
}
|
||||
}
|
105
src/org/envaya/sms/ui/CheckableRelativeLayout.java
Executable file
105
src/org/envaya/sms/ui/CheckableRelativeLayout.java
Executable 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
86
src/org/envaya/sms/ui/ForwardInbox.java
Executable file
86
src/org/envaya/sms/ui/ForwardInbox.java
Executable 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
35
src/org/envaya/sms/ui/Help.java
Executable 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));
|
||||
|
||||
}
|
||||
}
|
72
src/org/envaya/sms/ui/InertCheckBox.java
Executable file
72
src/org/envaya/sms/ui/InertCheckBox.java
Executable 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
129
src/org/envaya/sms/ui/Main.java
Executable 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
167
src/org/envaya/sms/ui/Prefs.java
Executable 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);
|
||||
}
|
||||
}
|
107
src/org/envaya/sms/ui/TestPhoneNumbers.java
Executable file
107
src/org/envaya/sms/ui/TestPhoneNumbers.java
Executable 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();
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user