package com.koushikdutta.desktopsms; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.net.URISyntaxException; import java.net.URL; import java.net.URLEncoder; import java.util.ArrayList; import java.util.Date; import java.util.HashSet; import java.util.Hashtable; import org.apache.http.client.ClientProtocolException; import org.codehaus.jackson.JsonEncoding; import org.codehaus.jackson.JsonFactory; import org.codehaus.jackson.JsonGenerator; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import android.accounts.AuthenticatorException; import android.accounts.OperationCanceledException; import android.app.PendingIntent; import android.app.Service; import android.content.ContentResolver; import android.content.ContentValues; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.database.ContentObserver; import android.database.Cursor; import android.graphics.Bitmap; import android.graphics.Bitmap.CompressFormat; import android.graphics.BitmapFactory; import android.graphics.BitmapFactory.Options; import android.net.Uri; import android.os.Handler; import android.os.IBinder; import android.provider.CallLog; import android.provider.CallLog.Calls; import android.provider.ContactsContract.CommonDataKinds; import android.provider.ContactsContract.Data; import android.provider.ContactsContract.PhoneLookup; import android.telephony.SmsManager; import android.text.format.DateFormat; import android.util.Base64; import android.util.Log; public class SyncService extends Service { private Settings mSettings; private static final String LOGTAG = SyncService.class.getSimpleName(); @Override public IBinder onBind(Intent intent) { return null; } static interface CursorGetter { void get(Cursor c, JSONObject j, String name, int index) throws JSONException; } public static final int INCOMING_SMS = 1; public static final int OUTGOING_SMS = 2; public static final int INCOMING_CALL = 1; public static final int OUTGOING_CALL = 2; public static final int MISSED_CALL = 3; static class SmsTypeMapper extends Hashtable<Integer, String> { { put(OUTGOING_SMS, "outgoing"); put(INCOMING_SMS, "incoming"); } public static SmsTypeMapper Instance = new SmsTypeMapper(); } static class CallTypeMapper extends Hashtable<Integer, String> { { put(MISSED_CALL, "missed"); put(OUTGOING_CALL, "outgoing"); put(INCOMING_CALL, "incoming"); } public static CallTypeMapper Instance = new CallTypeMapper(); } static class MmsImageType { } Hashtable<Class, CursorGetter> mapper = new Hashtable<Class, SyncService.CursorGetter>() { { put(int.class, new CursorGetter() { @Override public void get(Cursor c, JSONObject j, String name, int index) throws JSONException { j.put(name, c.getInt(index)); } }); put(long.class, new CursorGetter() { @Override public void get(Cursor c, JSONObject j, String name, int index) throws JSONException { j.put(name, c.getLong(index)); } }); put(String.class, new CursorGetter() { @Override public void get(Cursor c, JSONObject j, String name, int index) throws JSONException { j.put(name, c.getString(index)); } }); put(boolean.class, new CursorGetter() { @Override public void get(Cursor c, JSONObject j, String name, int index) throws JSONException { j.put(name, c.getInt(index) != 0); } }); put(SmsTypeMapper.class, new CursorGetter() { @Override public void get(Cursor c, JSONObject j, String name, int index) throws JSONException { j.put(name, SmsTypeMapper.Instance.get(c.getInt(index))); } }); put(CallTypeMapper.class, new CursorGetter() { @Override public void get(Cursor c, JSONObject j, String name, int index) throws JSONException { j.put(name, CallTypeMapper.Instance.get(c.getInt(index))); } }); put(MmsImageType.class, new CursorGetter() { @Override public void get(Cursor c, JSONObject j, String name, int index) throws JSONException { try { j.put("skip", true); int mmsId = c.getInt(c.getColumnIndex("_id")); Cursor convo = getContentResolver().query(Uri.parse("content://mms/" + mmsId + "/addr/"), null, null, null, null); try { if (!convo.moveToNext()) { return; } String number = convo.getString(convo.getColumnIndex("address")); if ("insert-address-token".equals(number)) return; j.put("number", number); } finally { convo.close(); } String selectionPart = "mid=" + mmsId; Uri uri = Uri.parse("content://mms/part"); Cursor cPart = getContentResolver().query(uri, null, selectionPart, null, null); try { while (cPart.moveToNext()) { String partId = cPart.getString(cPart.getColumnIndex("_id")); String type = cPart.getString(cPart.getColumnIndex("ct")); if ("image/jpeg".equals(type) || "image/bmp".equals(type) || "image/gif".equals(type) || "image/jpg".equals(type) || "image/png".equals(type)) { String image = getMmsImage(partId); if (image != null) { j.put("image", image); j.remove("skip"); return; } } } } finally { cPart.close(); } } catch (Exception ex) { ex.printStackTrace(); } } }); } }; String getMmsImage(String _id) { Uri partURI = Uri.parse("content://mms/part/" + _id); InputStream is = null; Bitmap bitmap = null; try { is = getContentResolver().openInputStream(partURI); BitmapFactory.Options options = new Options(); options.inSampleSize = 6; bitmap = BitmapFactory.decodeStream(is, null, options); ByteArrayOutputStream out = new ByteArrayOutputStream(); bitmap.compress(CompressFormat.PNG, 50, out); byte[] bytes = out.toByteArray(); String imageString = Base64.encodeToString(bytes, 0); return imageString; } catch (Exception e) { e.printStackTrace(); } finally { if (is != null) { try { is.close(); } catch (Exception e) { e.printStackTrace(); } } } return null; } Hashtable<String, Tuple<String, CursorGetter>> mmsmapper = new Hashtable<String, Tuple<String, CursorGetter>>() { { put("m_type", new Tuple<String, CursorGetter>("message", mapper.get(MmsImageType.class))); //put("seen", new Tuple<String, CursorGetter>("seen", mapper.get(int.class))); put("msg_box", new Tuple<String, CursorGetter>("type", mapper.get(SmsTypeMapper.class))); put("_id", new Tuple<String, CursorGetter>("id", mapper.get(long.class))); put("address", new Tuple<String, CursorGetter>("number", mapper.get(String.class))); put("read", new Tuple<String, CursorGetter>("read", mapper.get(boolean.class))); put("thread_id", new Tuple<String, CursorGetter>("thread_id", mapper.get(long.class))); } }; Hashtable<String, Tuple<String, CursorGetter>> smsmapper = new Hashtable<String, Tuple<String, CursorGetter>>() { { put("body", new Tuple<String, CursorGetter>("message", mapper.get(String.class))); //put("seen", new Tuple<String, CursorGetter>("seen", mapper.get(int.class))); put("type", new Tuple<String, CursorGetter>("type", mapper.get(SmsTypeMapper.class))); put("_id", new Tuple<String, CursorGetter>("id", mapper.get(long.class))); put("address", new Tuple<String, CursorGetter>("number", mapper.get(String.class))); put("read", new Tuple<String, CursorGetter>("read", mapper.get(boolean.class))); put("thread_id", new Tuple<String, CursorGetter>("thread_id", mapper.get(long.class))); } }; Hashtable<String, Tuple<String, CursorGetter>> callmapper = new Hashtable<String, Tuple<String, CursorGetter>>() { { put(Calls.TYPE, new Tuple<String, CursorGetter>("type", mapper.get(CallTypeMapper.class))); put(Calls._ID, new Tuple<String, CursorGetter>("id", mapper.get(long.class))); put(Calls.NUMBER, new Tuple<String, CursorGetter>("number", mapper.get(String.class))); put(Calls.DURATION, new Tuple<String, CursorGetter>("duration", mapper.get(int.class))); } }; CachedPhoneLookup getPhoneLookup(String number) { try { CachedPhoneLookup lookup = mLookup.get(number); if (lookup != null) return lookup; Uri curi = Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, Uri.encode(number)); Cursor c = getContentResolver().query(curi, null, null, null, null); try { if (c.moveToNext()) { String displayName = c.getString(c.getColumnIndex(PhoneLookup.DISPLAY_NAME)); String enteredNumber = c.getString(c.getColumnIndex(PhoneLookup.NUMBER)); if (!Helper.isJavaScriptNullOrEmpty(displayName)) { c.close(); lookup = new CachedPhoneLookup(); lookup.displayName = displayName; lookup.enteredNumber = ServiceHelper.numbersOnly(enteredNumber, true); // see if the user has a jabber contact for this address String jid = String.format("%s@desksms.appspotchat.com", lookup.enteredNumber); c = getContentResolver().query(Data.CONTENT_URI, new String[] { Data._ID }, String.format("%s = '%s' and %s = '%s'", Data.MIMETYPE, CommonDataKinds.Email.CONTENT_ITEM_TYPE, CommonDataKinds.Email.DATA, jid), null, null); lookup.hasDeskSMSContact = c.moveToNext(); c.close(); mLookup.put(number, lookup); return lookup; } } } finally { c.close(); } } catch (Exception ex) { } return null; } private static final class CachedPhoneLookup { public String displayName; public String enteredNumber; public boolean hasDeskSMSContact; } Hashtable<String, CachedPhoneLookup> mLookup = new Hashtable<String, CachedPhoneLookup>(); void sendUsingSmsManager(Context context, String number, String message, long date) { SmsManager sm = SmsManager.getDefault(); ArrayList<String> messages = sm.divideMessage(message); int messageCount = messages.size(); if (messageCount == 0) return; try { sm.sendMultipartTextMessage(number, null, messages, null, null); } catch (Exception e) { // some terrible firmware requires delivery intents. PendingIntent si = PendingIntent.getBroadcast(this, 2020, new Intent("com.koushikdutta.desktopsms.noop"), 0); PendingIntent di = PendingIntent.getBroadcast(this, 2020, new Intent("com.koushikdutta.desktopsms.noop"), 0); ArrayList<PendingIntent> sis = new ArrayList<PendingIntent>(); sis.add(si); ArrayList<PendingIntent> dis = new ArrayList<PendingIntent>(); dis.add(di); sm.sendMultipartTextMessage(number, null, messages, sis, dis); } ContentValues values = new ContentValues(); values.put("address", number); values.put("body", message); values.put("type", SyncService.OUTGOING_SMS); values.put("date", date); values.put("read", 1); context.getContentResolver().insert(Uri.parse("content://sms/sent"), values); } void sendUsingContentProvider(Context context, String number, String message, long date) throws Exception { ContentResolver r = context.getContentResolver(); //ContentProviderClient client = r.acquireContentProviderClient(Uri.parse("content://sms/queued")); ContentValues sv = new ContentValues(); sv.put("address", number); sv.put("date", date); sv.put("read", 1); sv.put("body", message); sv.put("type", SyncService.OUTGOING_SMS); String n = null; sv.put("subject", n); //sv.put("status", 32); Uri u = r.insert(Uri.parse("content://sms/queued"), sv); if (u == null) { throw new Exception(); } Intent bcast = new Intent("com.android.mms.transaction.SEND_MESSAGE"); bcast.setClassName("com.android.mms", "com.android.mms.transaction.SmsReceiverService"); context.startService(bcast); } private int sendOutbox(String outboxData) throws ClientProtocolException, OperationCanceledException, AuthenticatorException, IOException, URISyntaxException, JSONException { long maxOutboxSync = mLastOutboxSync; // the outbox MUST come in order, from the oldest to the newest. // TODO: this should be sorted just to sanity check I guess. JSONArray outbox; // LEGACY: we should always expect an envelope try { outbox = new JSONArray(outboxData); } catch (Exception ex){ JSONObject envelope = new JSONObject(outboxData); outbox = envelope.getJSONArray("data"); } if (outbox.length() == 0) { Log.i(LOGTAG, "Empty outbox"); return 0; } // Log.i(LOGTAG, "================Sending outbox================"); //Log.i(LOGTAG, outbox.toString(4)); for (int i = 0; i < outbox.length(); i++) { try { JSONObject sms = outbox.getJSONObject(i); String number = sms.getString("number"); String message = sms.getString("message"); // make sure that any messages we get are new messages long date = sms.getLong("date"); if (date <= mLastOutboxSync) continue; // Log.i(LOGTAG, sms.toString(4)); maxOutboxSync = Math.max(maxOutboxSync, date); sendUsingSmsManager(this, number, message, date); } catch (Exception ex) { ex.printStackTrace(); } } mLastOutboxSync = maxOutboxSync; mSettings.setLong("last_outbox_sync", maxOutboxSync); try { ServiceHelper.retryExecuteAndDisconnect(this, mAccount, new URL(String.format(ServiceHelper.OUTBOX_URL, mAccount) + "?max_date=" + mLastOutboxSync + "operation=DELETE"), null); } catch (Exception ex) { ex.printStackTrace(); } return outbox.length(); } private void retrieveOutbox() { try { String outbox = ServiceHelper.retryExecuteAsString(this, mAccount, new URL(String.format(ServiceHelper.OUTBOX_URL, mAccount) + "?min_date=" + mLastOutboxSync), null); sendOutbox(outbox); } catch (Exception ex) { ex.printStackTrace(); } } private void syncOutbox(String outbox) throws ClientProtocolException, OperationCanceledException, AuthenticatorException, IOException, URISyntaxException, JSONException { // Log.i(LOGTAG, "================Checking outbox================"); if (outbox == null) { retrieveOutbox(); } else { // if we got a push notifiation with an empty outbox, retrieve the outbox if (0 == sendOutbox(outbox)) retrieveOutbox(); } } private abstract class SyncBase { Uri contentProviderUri; String postUrl; String lastSyncSetting; Hashtable<String, Tuple<String, CursorGetter>> mapper; long dateScale = 1L; String incomingType = "incoming"; abstract void setSubject(JSONObject event, String displayName, Cursor cursor) throws JSONException; abstract void setMessage(JSONObject event, String displayName, Cursor cursor) throws JSONException; protected void logEvent(JSONObject event) throws JSONException { // System.out.println(event.toString(4)); } protected long getDate(Cursor c, JSONObject event, int dateColumn) { return c.getLong(dateColumn) * dateScale; } public void sync() throws Exception { long lastSync = mSettings.getLong(lastSyncSetting, 0); boolean isInitialSync = false; // i dont know if i want to do this, sync loop? //if (lastSync > System.currentTimeMillis()) // lastSync = 0; if (lastSync > 1309478400000L) lastSync = 0; if (lastSync != 0) { // make sure the id that we last used is still valid // users deleting messages can muck with ids. Cursor sanityCursor = getContentResolver().query(contentProviderUri, new String[] { "_id" }, null, null, "_id DESC LIMIT 1"); try { if (sanityCursor.moveToNext()) { long sanityId = sanityCursor.getLong(sanityCursor.getColumnIndex("_id")); if (sanityId < lastSync) { Log.i(LOGTAG, "Sanity check failed. Forcing initial sync."); Log.i(LOGTAG, "Sanity id: " + sanityId); Log.i(LOGTAG, "Id was: " + lastSync); lastSync = 0; mSettings.setLong(lastSyncSetting, 0); } } } finally { sanityCursor.close(); } } JsonFactory jf = new JsonFactory(); JsonGenerator gen = jf.createJsonGenerator(getFileStreamPath("sync.json"), JsonEncoding.UTF8); Cursor c; String threeDaysAgo = String.valueOf((System.currentTimeMillis() - 3L * 24L * 60L * 60L * 1000L) / dateScale); if (lastSync == 0) { isInitialSync = true; // only grab 3 days worth //lastSync = System.currentTimeMillis() - 3L * 24L * 60L * 60L * 1000L; c = getContentResolver().query(contentProviderUri, null, "date > ?", new String[] { threeDaysAgo }, null); } else { // we resume at the last id, but make sure that the last id isn't so ancient that it grabs a crapload of messages c = getContentResolver().query(contentProviderUri, null, "date > ? AND _id > ?", new String[] { threeDaysAgo, String.valueOf(lastSync) }, null); } // Log.i(LOGTAG, getClass().getSimpleName()); // Log.i(LOGTAG, String.valueOf(lastSync)); long latestEvent = lastSync; try { gen.writeStartObject(); gen.writeArrayFieldStart("data"); int eventCount = 0; String[] columnNames = c.getColumnNames(); int dateColumn = c.getColumnIndex("date"); int idColumn = c.getColumnIndex("_id"); while (c.moveToNext()) { try { JSONObject event = new JSONObject(); for (int i = 0; i < c.getColumnCount(); i++) { String name = columnNames[i]; Tuple<String, CursorGetter> tuple = mapper.get(name); if (tuple == null) continue; tuple.Second.get(c, event, tuple.First, i); } if (event.optBoolean("skip", false)) { // Log.i(LOGTAG, "=========Skipping event.========="); logEvent(event); continue; } String number = event.getString("number"); if(mBlacklist.getBoolean(number, false)) continue; long date = getDate(c, event, dateColumn); event.put("date", date); CachedPhoneLookup lookup = getPhoneLookup(number); String displayName; if (lookup != null) { if (mBlacklistedContacts.contains(lookup.enteredNumber)) continue; displayName = lookup.displayName; event.put("name", lookup.displayName); event.put("entered_number", lookup.enteredNumber); event.put("has_desksms_contact", lookup.hasDeskSMSContact); } else { displayName = number; } eventCount++; // only incoming events needs to be marked up with the subject and optionally a message (no-op for sms) if (event.getString("type").equals(incomingType)) { setSubject(event, displayName, c); setMessage(event, displayName, c); } logEvent(event); gen.writeRawValue(event.toString()); gen.flush(); long id = c.getLong(idColumn); latestEvent = Math.max(id, latestEvent); } catch (Exception ex) { ex.printStackTrace(); } } gen.writeEndArray(); if (eventCount == 0) { // Log.i(LOGTAG, "================No new messages================"); return; } // Log.i(LOGTAG, "================Forwarding inbox================"); gen.writeStringField("registration_id", "gcm:" + mRegistrationId); gen.writeBooleanField("is_initial_sync", isInitialSync); gen.writeNumberField("version_code", DesktopSMSApplication.mVersionCode); gen.writeNumberField("this_last_sync", lastSync); gen.writeNumberField("next_last_sync", latestEvent); gen.writeArrayFieldStart("registrations"); try { String registrations = mSettings.getString("registrations"); JSONObject r = new JSONObject(registrations); JSONArray names = r.names(); for (int i = 0; i < names.length(); i++) { String name = names.getString(i); String registration = r.getString(name); gen.writeString(registration); } } catch (Exception ex) { } gen.writeEndArray(); gen.writeEndObject(); } finally { c.close(); gen.close(); } File syncFile = getFileStreamPath("sync.json"); String results = ServiceHelper.retryExecuteAsString(SyncService.this, mAccount, new URL(String.format(postUrl, mAccount)), new ServiceHelper.FilePoster(syncFile)); JSONObject sr = new JSONObject(results); if (!sr.optBoolean("registered", true)) { mSettings.setBoolean("registered", false); mSettings.setString("account", null); mSettings.setString("registration_id", null); throw new Exception("not registered"); } // Log.i(LOGTAG, results); // Log.i(LOGTAG, "===== Updating last sync to " + latestEvent); mSettings.setLong(lastSyncSetting, latestEvent); } } class SmsSync extends SyncBase { public SmsSync() { contentProviderUri = Uri.parse("content://sms"); postUrl = ServiceHelper.SMS_URL; lastSyncSetting = "last_sms_sync"; mapper = smsmapper; } @Override void setSubject(JSONObject event, String displayName, Cursor cursor) throws JSONException { event.put("subject", getString(R.string.sms_received, displayName)); } @Override void setMessage(JSONObject event, String displayName, Cursor cursor) { } @Override protected long getDate(Cursor c, JSONObject event, int dateColumn) { long date = super.getDate(c, event, dateColumn); if ("incoming".equals(event.optString("type"))) { return date + mAdjustSmsDate; } return date; } } class MmsSync extends SyncBase { public MmsSync() { dateScale = 1000; contentProviderUri = Uri.parse("content://mms/"); postUrl = ServiceHelper.SMS_URL; lastSyncSetting = "last_mms_sync"; mapper = mmsmapper; } @Override void setSubject(JSONObject event, String displayName, Cursor cursor) throws JSONException { event.put("subject", getString(R.string.mms_received, displayName)); } @Override void setMessage(JSONObject event, String displayName, Cursor cursor) throws JSONException { event.put("message", getString(R.string.mms_received, displayName)); } protected void logEvent(JSONObject event) throws JSONException { Log.i(LOGTAG, "Forwarding MMS."); } } class CallSync extends SyncBase { public CallSync() { contentProviderUri = CallLog.Calls.CONTENT_URI; postUrl = ServiceHelper.CALL_URL; lastSyncSetting = "last_calls_sync"; mapper = callmapper; incomingType = "missed"; } @Override void setSubject(JSONObject event, String displayName, Cursor cursor) throws JSONException { event.put("subject", getString(R.string.missed_call_from, displayName)); } @Override void setMessage(JSONObject event, String displayName, Cursor cursor) throws JSONException { long date = event.getLong("date"); java.text.DateFormat df = DateFormat.getTimeFormat(SyncService.this); String dateString = df.format(new Date(date)); event.put("message", getString(R.string.missed_call_at, dateString)); } } long mLastOutboxSync; String mAccount; Handler mHandler = new Handler(); Thread mSyncThread = null; String mPendingOutbox; long mSyncStart = 0; boolean mPendingOutboxSync; String mRegistrationId; long mAdjustSmsDate; SharedPreferences mBlacklist; HashSet<String> mBlacklistedContacts; private void sync(final Intent intent) { mBlacklist = getSharedPreferences("blacklist", MODE_PRIVATE); mBlacklistedContacts = new HashSet<String>(); for (String number: mBlacklist.getAll().keySet()) { CachedPhoneLookup lookup = getPhoneLookup(number); if (lookup != null) mBlacklistedContacts.add(lookup.enteredNumber); } Log.i(LOGTAG, "Version: " + DesktopSMSApplication.mVersionCode); // for the very first startup of the service, we set the first start as sms, to flush anything pending. final String reason = mFirstStart ? "sms" : intent.getStringExtra("reason"); mFirstStart = false; // no reason? this is just a 15 min repeating wakeup call then. if (reason == null) { // Log.i(LOGTAG, "No reason for sync"); return; } // Log.i(LOGTAG, "============= Sync Reason " + reason + "============="); boolean xmpp = mSettings.getBoolean("forward_xmpp", true); boolean email = mSettings.getBoolean("forward_email", true); boolean web = mSettings.getBoolean("forward_web", true); if (!xmpp && !email && !web) { Log.i(LOGTAG, "All forwarding options are disabled."); return; } mPendingOutbox = intent.getStringExtra("outbox"); mPendingOutboxSync = "outbox".equals(reason); mSyncStart = System.currentTimeMillis(); synchronized (this) { if (mSyncThread != null) { // Log.i(LOGTAG, "Sync is already running."); return; } mAdjustSmsDate = mSettings.getInt("adjust_sms_date", 0) * 60L * 60L * 1000L; mRegistrationId = mSettings.getString("registration_id"); mAccount = mSettings.getString("account"); // this defaults to true because this flag used to not exist // and upgraded clients will stop syncing. boolean registered = mSettings.getBoolean("registered", true); if (mAccount == null || mRegistrationId == null || !registered) { Log.i(LOGTAG, "User is not registered."); return; } mLastOutboxSync = mSettings.getLong("last_outbox_sync", 0); mSyncThread = new Thread() { @Override public void run() { WakeLock.acquirePartial(SyncService.this); try { // if we are starting for the outbox, do that immediately boolean startedForOutbox = mPendingOutboxSync; boolean startedForPhoneState = "phone".equals(reason); boolean startedForSms = "sms".equals(reason); if (mPendingOutboxSync) { mPendingOutboxSync = false; syncOutbox(mPendingOutbox); mPendingOutbox = null; } while (mSyncStart + 15000L > System.currentTimeMillis()) { mSmsSyncer.sync(); mCallSyncer.sync(); mMmsSyncer.sync(); // however, if an outbox message comes in while we are polling, // let's send it if (mPendingOutboxSync) { // Log.i(LOGTAG, "================Outbox ping received================"); mPendingOutboxSync = false; syncOutbox(mPendingOutbox); mPendingOutbox = null; } Thread.sleep(3000); } // if we did not start for the outbox, sync it now just in case // but don't do it on phone state change. if (startedForSms) { syncOutbox(mPendingOutbox); mPendingOutbox = null; } } catch (Exception ex) { ex.printStackTrace(); } finally { mHandler.post(new Runnable() { @Override public void run() { mSyncThread = null; } }); WakeLock.release(); } } }; mSyncThread.start(); } } SmsSync mSmsSyncer = new SmsSync(); MmsSync mMmsSyncer = new MmsSync(); CallSync mCallSyncer = new CallSync(); ContentObserver mMmsObserver = new ContentObserver(mHandler) { @Override public void onChange(boolean selfChange) { super.onChange(selfChange); Intent intent = new Intent(); intent.putExtra("reason", "sms"); sync(intent); } }; ContentObserver mSmsObserver = new ContentObserver(mHandler) { @Override public void onChange(boolean selfChange) { super.onChange(selfChange); Intent intent = new Intent(); intent.putExtra("reason", "sms"); sync(intent); } }; ContentObserver mCallsObserver = new ContentObserver(mHandler) { @Override public void onChange(boolean selfChange) { super.onChange(selfChange); Intent intent = new Intent(); intent.putExtra("reason", "phone"); sync(intent); } }; public void onDestroy() { getContentResolver().unregisterContentObserver(mSmsObserver); getContentResolver().unregisterContentObserver(mCallsObserver); getContentResolver().unregisterContentObserver(mMmsObserver); } @Override public void onCreate() { super.onCreate(); TickleServiceHelper.registerForPush(this, null); mSettings = Settings.getInstance(this); getContentResolver().registerContentObserver(mSmsSyncer.contentProviderUri, true, mSmsObserver); getContentResolver().registerContentObserver(mCallSyncer.contentProviderUri, true, mCallsObserver); getContentResolver().registerContentObserver(mMmsSyncer.contentProviderUri, true, mCallsObserver); } boolean mFirstStart = true; @Override public int onStartCommand(Intent intent, int flags, int startId) { Log.i(LOGTAG, "Service starting"); if (intent != null) { sync(intent); return START_STICKY; } return super.onStartCommand(intent, flags, startId); } }