/*
* Copyright (C) 2012 Louis Fazen
*
* 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 com.alphabetbloc.accessadmin.services;
import java.util.ArrayList;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import android.app.Activity;
import android.app.PendingIntent;
import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.database.ContentObserver;
import android.database.Cursor;
import android.net.Uri;
import android.os.IBinder;
import android.preference.PreferenceManager;
import android.telephony.SmsManager;
import android.util.Log;
import com.alphabetbloc.accessadmin.data.Constants;
import com.alphabetbloc.accessadmin.data.EncryptedPreferences;
/**
* Do NOT call on its own, should only be called with a repeating wakelock alarm
* through the Device Admin Service (e.g. with SEND_SMS intent extra). <br>
* <br>
* Service sends SMS, checks for it being sent, waits for it to be logged in the
* outbox, and then deletes the SMS. Can be called more than once, will wait for
* a default time before killing itself, but no longer than 6x the default time
* after the last pending SMS has been registered with the service. Intent
* requires default of smsType, smsLine, and smsMessage Intent Extras.
*
* @author Louis Fazen (louis.fazen@gmail.com)
*
*/
public class SendSMSService extends Service {
public static final String TAG = "SendSMSService";
private static final String SMS_SENT = "SMS_SENT";
public static final String SMS_REPLY_PREFIX = "!Reply!: ";
private Context mContext;
private ArrayList<SMS> mPendingSms = new ArrayList<SMS>();
private SMSSentReceiver mSmsSentReceiver;
private ScheduledExecutorService mExecutor = Executors.newScheduledThreadPool(5);
private SentOutboxObserver mSentObserver = null;
private ContentResolver mContentResolver = null;
private SMS mCurrentSms;
private boolean mSentSms;
private boolean mDeletedSms;
private int mMessageCount;
private int mDeleteCount;
@Override
public IBinder onBind(Intent intent) {
// Auto-generated method stub
return null;
}
@Override
public void onCreate() {
super.onCreate();
mContext = this;
mMessageCount = 0;
mDeleteCount = 0;
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
mMessageCount++;
final SharedPreferences prefs = new EncryptedPreferences(mContext, mContext.getSharedPreferences(Constants.ENCRYPTED_PREFS, Context.MODE_PRIVATE));
if (intent != null) {
int broadcast = intent.getIntExtra(Constants.DEVICE_ADMIN_WORK, 0);
boolean needsConfirmation = intent.getBooleanExtra(Constants.SMS_SENT_CONFIRMATION, false);
String phoneNumber = intent.getStringExtra(Constants.SMS_LINE);
String message = intent.getStringExtra(Constants.SMS_MESSAGE);
if (phoneNumber == null)
phoneNumber = prefs.getString(Constants.SMS_REPLY_LINE, Constants.DEFAULT_SMS_REPLY_LINE);
if (message == null)
message = "Message has been lost";
SMS sms = new SMS(mMessageCount, needsConfirmation, broadcast, phoneNumber, message);
mPendingSms.add(sms);
if (Constants.DEBUG)
Log.v(TAG, "SendSMSService.onStartCommand Called with " + "\n\t MESSAGE= " + message + "\n\t TO= \'" + phoneNumber + "\'" + "\n\t PENDING SMS SIZE=" + mPendingSms.size() + "\n\t MESSAGE COUNT=" + mMessageCount);
if (mMessageCount == 1) {
setupReceivers();
sendNextSms();
if (Constants.DEBUG)
Log.v(TAG, "SendSMSService Setting up receivers and sending First SMS");
}
}
return super.onStartCommand(intent, flags, startId);
}
private void setupReceivers() {
// Receive notification when message sends
if (mSmsSentReceiver == null) {
mSmsSentReceiver = new SMSSentReceiver();
registerReceiver(mSmsSentReceiver, new IntentFilter(SMS_SENT));
}
// Receive notification when/if message is entered into SMS 'Sent Box'
// N.B. Deals with few AOSP mods that receive all outgoing SMS in outbox
// SMS sent programmatically usually do not enter SMS app's SENT.db
if (mSentObserver == null) {
mSentObserver = new SentOutboxObserver();
mContentResolver = mContext.getContentResolver();
mContentResolver.registerContentObserver(Uri.parse("content://sms"), true, mSentObserver);
}
}
public void sendNextSms() {
mCurrentSms = mPendingSms.get(0);
mSentSms = false;
mDeletedSms = false;
mSmsSentReceiver.addSMS(mCurrentSms);
// Send Sms
SmsManager smsManager = SmsManager.getDefault();
PendingIntent sentPI = PendingIntent.getBroadcast(this, 0, new Intent(SMS_SENT), 0);
smsManager.sendTextMessage(mCurrentSms.getNumber(), null, mCurrentSms.getMessage(), sentPI, null);
if (Constants.DEBUG)
Log.v(TAG, "SENDING NEW SMS!: \n\t NUMBER =" + mCurrentSms.getNumber() + "\n\t MESSAGE=" + mCurrentSms.getMessage());
// Monitor Delivery
updateSmsDelivery();
}
private void updateSmsDelivery() {
mExecutor.schedule(new Runnable() {
int count = 0;
public void run() {
boolean completedSms = mSentSms & mDeletedSms; // don't
// short-circuit
if (Constants.DEBUG)
Log.v(TAG, "updateSmsDelivery with: \n\t SmsSent =" + mSentSms + "\n\t SmsDeleted=" + mDeletedSms);
if (!completedSms && count < 60) {
mExecutor.schedule(this, 3000, TimeUnit.MILLISECONDS);
if (Constants.DEBUG)
Log.v(TAG, "Wait another 3 seconds. Current cycle count=" + count);
count++;
} else {
// Log errors
if (Constants.DEBUG) {
if (!mSentSms)
Log.e(TAG, "Timed out after 3 minutes of waiting for SMS to send");
else if (!mDeletedSms)
Log.e(TAG, "Timed out after 3 minutes of waiting for SMS to delete");
else
Log.i(TAG, "SMS has been successfully sent and deleted from outbox");
}
// delete the SMS if haven't done so
if (!mSentSms && mPendingSms.size() > 0) {
if (Constants.DEBUG)
Log.e(TAG, "COULD NOT SEND SMS!! Removing SMS 0 from mPendingSms: \n\t SMS MESSAGE=" + mPendingSms.get(0).getMessage());
mPendingSms.remove(0);
}
if (Constants.DEBUG)
Log.v(TAG, "SMS messages that are still pending=" + mPendingSms.size());
// send next SMS, if there is one
if (mPendingSms.size() > 0)
sendNextSms();
else
stopService();
}
}
}, 0, TimeUnit.MILLISECONDS);
}
public class SMSSentReceiver extends BroadcastReceiver {
SMS sms = null;
public void addSMS(SMS currentSms) {
sms = currentSms;
}
@Override
public void onReceive(Context ctxt, Intent intent) {
int result = getResultCode();
if (result == Activity.RESULT_OK) {
// log last successfully sent message
mSentSms = true;
if (sms.needsConfirmation()) {
SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(mContext);
pref.edit().putString(String.valueOf(sms.getBroadcast()), sms.getMessage()).commit();
}
for (SMS currentSms : mPendingSms) {
if (sms.getId() == currentSms.getId()) {
int oldtotal = mPendingSms.size();
mPendingSms.remove(currentSms);
if (Constants.DEBUG)
Log.v(TAG, "Removed an SMS from the Pending List (1/" + oldtotal + ") total. Removed SMS had \n\t ID=" + sms.getId() + "\n\t MESSAGE=" + sms.getMessage() + "\n\t Number of SMS that are still on pending list=" + mPendingSms.size());
break;
}
}
}
}
}
// Need observer b/c delay between receive and move to outbox
class SentOutboxObserver extends ContentObserver {
public SentOutboxObserver() {
super(null);
if (Constants.DEBUG)
Log.v(TAG, "New SentOutbox Observer Created!");
}
@Override
public void onChange(boolean selfChange) {
if (Constants.DEBUG)
Log.i(TAG, "Change to SMS Outbox. Calling deleteSmsFromOutbox to delete single SMS");
deleteSmsFromOutbox(false);
super.onChange(selfChange);
}
}
public void deleteSmsFromOutbox(boolean deleteAll) {
final SharedPreferences prefs = new EncryptedPreferences(this, this.getSharedPreferences(Constants.ENCRYPTED_PREFS, Context.MODE_PRIVATE));
String line = null;
String message = null;
if (deleteAll) {
line = prefs.getString(Constants.SMS_REPLY_LINE, "");
message = SMS_REPLY_PREFIX;
} else {
line = mCurrentSms.getNumber();
message = mCurrentSms.getMessage();
}
if (Constants.DEBUG)
Log.v(TAG, "Calling deleteSmsFromOutbox with: \n\t DELETE-ALL=" + deleteAll + "\n\t LINE=" + line + "\n\t MESSAGE=" + message);
try {
Uri uriSms = Uri.parse("content://sms");
Cursor c = mContext.getContentResolver().query(uriSms, new String[] { "_id", "address", "body" }, "address =? ", new String[] { line }, null);
if (c != null && c.moveToFirst()) {
do {
long id = c.getLong(c.getColumnIndex("_id"));
String address = c.getString(c.getColumnIndex("address"));
String body = c.getString(c.getColumnIndex("body"));
if (Constants.DEBUG)
Log.v(TAG, "Found SMS in the Outbox: \n\t ID=" + id + "\n\t ADDRESS=" + address + "\n\t BODY=" + body);
if (body.contains(message) && address.equalsIgnoreCase(line)) {
int rows = mContext.getContentResolver().delete(Uri.parse("content://sms/" + id), null, null);
mDeletedSms = true;
mDeleteCount++;
if (Constants.DEBUG)
Log.v(TAG, "Successfully deleted " + rows + " sms from the outbox");
}
} while (c.moveToNext());
}
c.close();
} catch (Exception e) {
if (Constants.DEBUG)
Log.e(TAG, "Could not delete SMS from inbox: " + e.getMessage());
}
}
// JUnit Testing
public ArrayList<SMS> getPendingSMS() {
return mPendingSms;
}
private void stopService() {
if (mMessageCount >= mDeleteCount)
deleteSmsFromOutbox(true);
stopSelf();
}
@Override
public void onDestroy() {
if (Constants.DEBUG)
Log.v(TAG, "Ending the SendSmsService");
unregisterReceiver(mSmsSentReceiver);
mSmsSentReceiver = null;
mContentResolver.unregisterContentObserver(mSentObserver);
mSentObserver = null;
super.onDestroy();
}
// JUnit Testing, had to make this class public
public class SMS {
private int smsBroadcast;
private int smsId;
private boolean smsConfirmation;
private String smsNumber;
private String smsMessage;
public SMS(int id, boolean confirmation, int broadcast, String phoneNumber, String message) {
smsBroadcast = broadcast;
smsConfirmation = confirmation;
smsNumber = phoneNumber;
smsMessage = SMS_REPLY_PREFIX + message;
smsId = id;
}
public int getBroadcast() {
return smsBroadcast;
}
public boolean needsConfirmation() {
return smsConfirmation;
}
public String getNumber() {
return smsNumber;
}
public String getMessage() {
return smsMessage;
}
public int getId() {
return smsId;
}
}
// SEND SMS METHOD
// ---when the SMS has been delivered---
// Delivery intent: could add later, but does not add much purpose
// do via xml so service does not linger waiting for receiver to finish
// http://stackoverflow.com/questions/5624470/enable-and-disable-a-broadcast-receiver
// String DELIVERED = "SMS_DELIVERED";
// PendingIntent deliveredPI = PendingIntent.getBroadcast(this, 0, new
// Intent(DELIVERED), 0);
// SMSDeliverReceiver deliveredSMS = new SMSDeliverReceiver();
// deliveredSMS.addSMS(phoneNumber, body);
// registerReceiver(deliveredSMS, new IntentFilter(DELIVERED));
}