diff options
Diffstat (limited to 'src/com/android/bluetooth/map/BluetoothMapContentObserver.java')
-rw-r--r-- | src/com/android/bluetooth/map/BluetoothMapContentObserver.java | 1577 |
1 files changed, 1188 insertions, 389 deletions
diff --git a/src/com/android/bluetooth/map/BluetoothMapContentObserver.java b/src/com/android/bluetooth/map/BluetoothMapContentObserver.java index 204bb0239..7101f220b 100644 --- a/src/com/android/bluetooth/map/BluetoothMapContentObserver.java +++ b/src/com/android/bluetooth/map/BluetoothMapContentObserver.java @@ -1,5 +1,5 @@ /* -* Copyright (C) 2013 Samsung System LSI +* Copyright (C) 2014 Samsung System LSI * 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 @@ -14,36 +14,47 @@ */ package com.android.bluetooth.map; -import java.io.ByteArrayInputStream; import java.io.FileNotFoundException; +import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; import java.io.StringWriter; import java.io.UnsupportedEncodingException; import java.util.ArrayList; import java.util.Arrays; +import java.util.Calendar; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; +import javax.obex.ResponseCodes; + import org.xmlpull.v1.XmlSerializer; import android.app.Activity; import android.app.PendingIntent; import android.content.BroadcastReceiver; +import android.content.ContentProviderClient; import android.content.ContentResolver; import android.content.ContentUris; import android.content.ContentValues; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; +import android.content.IntentFilter.MalformedMimeTypeException; import android.database.ContentObserver; import android.database.Cursor; import android.net.Uri; +import android.os.Build; import android.os.Handler; +import android.os.Message; +import android.os.ParcelFileDescriptor; +import android.os.RemoteException; import android.provider.BaseColumns; +import com.android.bluetooth.mapapi.BluetoothMapContract; +import com.android.bluetooth.mapapi.BluetoothMapContract.MessageColumns; import android.provider.Telephony; import android.provider.Telephony.Mms; import android.provider.Telephony.MmsSms; @@ -54,34 +65,59 @@ import android.telephony.ServiceState; import android.telephony.SmsManager; import android.telephony.SmsMessage; import android.telephony.TelephonyManager; +import android.text.format.DateUtils; import android.util.Log; import android.util.Xml; import android.os.Looper; import com.android.bluetooth.map.BluetoothMapUtils.TYPE; -import com.android.bluetooth.map.BluetoothMapbMessageMmsEmail.MimePart; +import com.android.bluetooth.map.BluetoothMapbMessageMms.MimePart; import com.google.android.mms.pdu.PduHeaders; public class BluetoothMapContentObserver { private static final String TAG = "BluetoothMapContentObserver"; - private static final boolean D = false; - private static final boolean V = false; + private static final boolean D = BluetoothMapService.DEBUG; + private static final boolean V = BluetoothMapService.VERBOSE; + + private static final String EVENT_TYPE_DELETE = "MessageDeleted"; + private static final String EVENT_TYPE_SHIFT = "MessageShift"; + private static final String EVENT_TYPE_NEW = "NewMessage"; + private static final String EVENT_TYPE_DELEVERY_SUCCESS = "DeliverySuccess"; + private static final String EVENT_TYPE_SENDING_SUCCESS = "SendingSuccess"; + private static final String EVENT_TYPE_SENDING_FAILURE = "SendingFailure"; + private static final String EVENT_TYPE_DELIVERY_FAILURE = "DeliveryFailure"; + + + private static final long PROVIDER_ANR_TIMEOUT = 20 * DateUtils.SECOND_IN_MILLIS; private Context mContext; private ContentResolver mResolver; + private ContentProviderClient mProviderClient = null; private BluetoothMnsObexClient mMnsClient; + private BluetoothMapMasInstance mMasInstance = null; private int mMasId; + private boolean mEnableSmsMms = false; + private boolean mObserverRegistered = false; + private BluetoothMapEmailSettingsItem mAccount; + private String mAuthority = null; + + private BluetoothMapFolderElement mFolders = + new BluetoothMapFolderElement("DUMMY", null); // Will be set by the MAS when generated. + private Uri mMessageUri = null; public static final int DELETED_THREAD_ID = -1; - /* X-Mms-Message-Type field types. These are from PduHeaders.java */ + // X-Mms-Message-Type field types. These are from PduHeaders.java public static final int MESSAGE_TYPE_RETRIEVE_CONF = 0x84; + // Text only MMS converted to SMS if sms parts less than or equal to defined count + private static final int CONVERT_MMS_TO_SMS_PART_COUNT = 10; + private TYPE mSmsType; static final String[] SMS_PROJECTION = new String[] { - BaseColumns._ID, + Sms._ID, Sms.THREAD_ID, Sms.ADDRESS, Sms.BODY, @@ -90,30 +126,60 @@ public class BluetoothMapContentObserver { Sms.TYPE, Sms.STATUS, Sms.LOCKED, - Sms.ERROR_CODE, + Sms.ERROR_CODE }; - static final String[] MMS_PROJECTION = new String[] { - BaseColumns._ID, + static final String[] SMS_PROJECTION_SHORT = new String[] { + Sms._ID, + Sms.THREAD_ID, + Sms.TYPE + }; + + static final String[] MMS_PROJECTION_SHORT = new String[] { + Mms._ID, Mms.THREAD_ID, - Mms.MESSAGE_ID, - Mms.MESSAGE_SIZE, - Mms.SUBJECT, - Mms.CONTENT_TYPE, - Mms.TEXT_ONLY, - Mms.DATE, - Mms.DATE_SENT, - Mms.READ, - Mms.MESSAGE_BOX, Mms.MESSAGE_TYPE, - Mms.STATUS, + Mms.MESSAGE_BOX }; - public BluetoothMapContentObserver(final Context context) { + static final String[] EMAIL_PROJECTION_SHORT = new String[] { + BluetoothMapContract.MessageColumns._ID, + BluetoothMapContract.MessageColumns.FOLDER_ID, + BluetoothMapContract.MessageColumns.FLAG_READ + }; + + + public BluetoothMapContentObserver(final Context context, + BluetoothMnsObexClient mnsClient, + BluetoothMapMasInstance masInstance, + BluetoothMapEmailSettingsItem account, + boolean enableSmsMms) throws RemoteException { mContext = context; mResolver = mContext.getContentResolver(); + mAccount = account; + mMasInstance = masInstance; + mMasId = mMasInstance.getMasId(); + if(account != null) { + mAuthority = Uri.parse(account.mBase_uri).getAuthority(); + mMessageUri = Uri.parse(account.mBase_uri + "/" + BluetoothMapContract.TABLE_MESSAGE); + mProviderClient = mResolver.acquireUnstableContentProviderClient(mAuthority); + if (mProviderClient == null) { + throw new RemoteException("Failed to acquire provider for " + mAuthority); + } + mProviderClient.setDetectNotResponding(PROVIDER_ANR_TIMEOUT); + } + mEnableSmsMms = enableSmsMms; mSmsType = getSmsType(); + mMnsClient = mnsClient; + } + + /** + * Set the folder structure to be used for this instance. + * @param folderStructure + */ + public void setFolderStructure(BluetoothMapFolderElement folderStructure) { + this.mFolders = folderStructure; } private TYPE getSmsType() { @@ -140,28 +206,28 @@ public class BluetoothMapContentObserver { if (V) Log.d(TAG, "onChange on thread: " + Thread.currentThread().getId() + " Uri: " + uri.toString() + " selfchange: " + selfChange); - handleMsgListChanges(); + handleMsgListChanges(uri); } }; private static final String folderSms[] = { "", - "inbox", - "sent", - "draft", - "outbox", - "outbox", - "outbox", - "inbox", - "inbox", + BluetoothMapContract.FOLDER_NAME_INBOX, + BluetoothMapContract.FOLDER_NAME_SENT, + BluetoothMapContract.FOLDER_NAME_DRAFT, + BluetoothMapContract.FOLDER_NAME_OUTBOX, + BluetoothMapContract.FOLDER_NAME_OUTBOX, + BluetoothMapContract.FOLDER_NAME_OUTBOX, + BluetoothMapContract.FOLDER_NAME_INBOX, + BluetoothMapContract.FOLDER_NAME_INBOX, }; private static final String folderMms[] = { "", - "inbox", - "sent", - "draft", - "outbox", + BluetoothMapContract.FOLDER_NAME_INBOX, + BluetoothMapContract.FOLDER_NAME_SENT, + BluetoothMapContract.FOLDER_NAME_DRAFT, + BluetoothMapContract.FOLDER_NAME_OUTBOX, }; private class Event { @@ -171,18 +237,28 @@ public class BluetoothMapContentObserver { String oldFolder; TYPE msgType; + final static String PATH = "telecom/msg/"; + public Event(String eventType, long handle, String folder, String oldFolder, TYPE msgType) { - String PATH = "telecom/msg/"; + this.eventType = eventType; this.handle = handle; if (folder != null) { - this.folder = PATH + folder; + if(msgType == TYPE.EMAIL) { + this.folder = folder; + } else { + this.folder = PATH + folder; + } } else { this.folder = null; } if (oldFolder != null) { - this.oldFolder = PATH + oldFolder; + if(msgType == TYPE.EMAIL) { + this.oldFolder = oldFolder; + } else { + this.oldFolder = PATH + oldFolder; + } } else { this.oldFolder = null; } @@ -195,7 +271,7 @@ public class BluetoothMapContentObserver { try { xmlEvtReport.setOutput(sw); xmlEvtReport.startDocument(null, null); - xmlEvtReport.text("\n"); + xmlEvtReport.text("\r\n"); xmlEvtReport.startTag("", "MAP-event-report"); xmlEvtReport.attribute("", "version", "1.0"); @@ -214,14 +290,14 @@ public class BluetoothMapContentObserver { xmlEvtReport.endTag("", "MAP-event-report"); xmlEvtReport.endDocument(); } catch (IllegalArgumentException e) { - e.printStackTrace(); + if(D) Log.w(TAG,e); } catch (IllegalStateException e) { - e.printStackTrace(); + if(D) Log.w(TAG,e); } catch (IOException e) { - e.printStackTrace(); + if(D) Log.w(TAG,e); } - if (V) System.out.println(sw.toString()); + if (V) Log.d(TAG, sw.toString()); return sw.toString().getBytes("UTF-8"); } @@ -229,33 +305,125 @@ public class BluetoothMapContentObserver { private class Msg { long id; - int type; - - public Msg(long id, int type) { + int type; // Used as folder for SMS/MMS + int threadId; // Used for SMS/MMS at delete + long folderId = -1; // Email folder ID + long oldFolderId = -1; // Used for email undelete + boolean localInitiatedSend = false; // Used for MMS to filter out events + boolean transparent = false; // Used for EMAIL to delete message sent with transparency + + public Msg(long id, int type, int threadId) { this.id = id; this.type = type; + this.threadId = threadId; + } + public Msg(long id, long folderId) { + this.id = id; + this.folderId = folderId; + } + + /* Eclipse generated hashCode() and equals() to make + * hashMap lookup work independent of whether the obj + * is used for email or SMS/MMS and whether or not the + * oldFolder is set. */ + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + (int) (id ^ (id >>> 32)); + return result; } - } - private Map<Long, Msg> mMsgListSms = - Collections.synchronizedMap(new HashMap<Long, Msg>()); + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + Msg other = (Msg) obj; + if (id != other.id) + return false; + return true; + } + } - private Map<Long, Msg> mMsgListMms = - Collections.synchronizedMap(new HashMap<Long, Msg>()); + private Map<Long, Msg> mMsgListSms = new HashMap<Long, Msg>(); + + private Map<Long, Msg> mMsgListMms = new HashMap<Long, Msg>(); + + private Map<Long, Msg> mMsgListEmail = new HashMap<Long, Msg>(); + + public int setNotificationRegistration(int notificationStatus) throws RemoteException { + // Forward the request to the MNS thread as a message - including the MAS instance ID. + if(D) Log.d(TAG,"setNotificationRegistration() enter"); + Handler mns = mMnsClient.getMessageHandler(); + if(mns != null) { + Message msg = mns.obtainMessage(); + msg.what = BluetoothMnsObexClient.MSG_MNS_NOTIFICATION_REGISTRATION; + msg.arg1 = mMasId; + msg.arg2 = notificationStatus; + mns.sendMessageDelayed(msg, 10); // Send message without forcing a context switch + /* Some devices - e.g. PTS needs to get the unregister confirm before we actually + * disconnect the MNS. */ + if(D) Log.d(TAG,"setNotificationRegistration() MSG_MNS_NOTIFICATION_REGISTRATION send to MNS"); + } else { + // This should not happen except at shutdown. + if(D) Log.d(TAG,"setNotificationRegistration() Unable to send registration request"); + return ResponseCodes.OBEX_HTTP_UNAVAILABLE; + } + if(notificationStatus == BluetoothMapAppParams.NOTIFICATION_STATUS_YES) { + registerObserver(); + } else { + unregisterObserver(); + } + return ResponseCodes.OBEX_HTTP_OK; + } - public void registerObserver(BluetoothMnsObexClient mns, int masId) { + public void registerObserver() throws RemoteException{ if (V) Log.d(TAG, "registerObserver"); + + if (mObserverRegistered) + return; + /* Use MmsSms Uri since the Sms Uri is not notified on deletes */ - mMasId = masId; - mMnsClient = mns; - mResolver.registerContentObserver(MmsSms.CONTENT_URI, false, mObserver); + if(mEnableSmsMms){ + //this is sms/mms + mResolver.registerContentObserver(MmsSms.CONTENT_URI, false, mObserver); + mObserverRegistered = true; + } + if(mAccount != null) { + + mProviderClient = mResolver.acquireUnstableContentProviderClient(mAuthority); + if (mProviderClient == null) { + throw new RemoteException("Failed to acquire provider for " + mAuthority); + } + mProviderClient.setDetectNotResponding(PROVIDER_ANR_TIMEOUT); + + /* For URI's without account ID */ + Uri uri = Uri.parse(mAccount.mBase_uri_no_account + "/" + BluetoothMapContract.TABLE_MESSAGE); + if(D) Log.d(TAG, "Registering observer for: " + uri); + mResolver.registerContentObserver(uri, true, mObserver); + + /* For URI's with account ID - is handled the same way as without ID, but is + * only triggered for MAS instances with matching account ID. */ + uri = Uri.parse(mAccount.mBase_uri + "/" + BluetoothMapContract.TABLE_MESSAGE); + if(D) Log.d(TAG, "Registering observer for: " + uri); + mResolver.registerContentObserver(uri, true, mObserver); + mObserverRegistered = true; + } initMsgList(); } public void unregisterObserver() { if (V) Log.d(TAG, "unregisterObserver"); mResolver.unregisterContentObserver(mObserver); - mMnsClient = null; + mObserverRegistered = false; + if(mProviderClient != null){ + mProviderClient.release(); + mProviderClient = null; + } } private void sendEvent(Event evt) { @@ -274,47 +442,73 @@ public class BluetoothMapContentObserver { } } - private void initMsgList() { + private void initMsgList() throws RemoteException { if (V) Log.d(TAG, "initMsgList"); - mMsgListSms.clear(); - mMsgListMms.clear(); - - HashMap<Long, Msg> msgListSms = new HashMap<Long, Msg>(); + if(mEnableSmsMms) { - Cursor c = mResolver.query(Sms.CONTENT_URI, - SMS_PROJECTION, null, null, null); + HashMap<Long, Msg> msgListSms = new HashMap<Long, Msg>(); - if (c != null && c.moveToFirst()) { - do { - long id = c.getLong(c.getColumnIndex(BaseColumns._ID)); - int type = c.getInt(c.getColumnIndex(Sms.TYPE)); + Cursor c = mResolver.query(Sms.CONTENT_URI, + SMS_PROJECTION_SHORT, null, null, null); - Msg msg = new Msg(id, type); - msgListSms.put(id, msg); - } while (c.moveToNext()); - c.close(); - } + if (c != null && c.moveToFirst()) { + do { + long id = c.getLong(c.getColumnIndex(Sms._ID)); + int type = c.getInt(c.getColumnIndex(Sms.TYPE)); + int threadId = c.getInt(c.getColumnIndex(Sms.THREAD_ID)); - mMsgListSms = msgListSms; + Msg msg = new Msg(id, type, threadId); + msgListSms.put(id, msg); + } while (c.moveToNext()); + c.close(); + } + synchronized(mMsgListSms) { + mMsgListSms.clear(); + mMsgListSms = msgListSms; + } - HashMap<Long, Msg> msgListMms = new HashMap<Long, Msg>(); + HashMap<Long, Msg> msgListMms = new HashMap<Long, Msg>(); - c = mResolver.query(Mms.CONTENT_URI, - MMS_PROJECTION, null, null, null); + c = mResolver.query(Mms.CONTENT_URI, MMS_PROJECTION_SHORT, null, null, null); - if (c != null && c.moveToFirst()) { - do { - long id = c.getLong(c.getColumnIndex(BaseColumns._ID)); - int type = c.getInt(c.getColumnIndex(Mms.MESSAGE_BOX)); + if (c != null && c.moveToFirst()) { + do { + long id = c.getLong(c.getColumnIndex(Mms._ID)); + int type = c.getInt(c.getColumnIndex(Mms.MESSAGE_BOX)); + int threadId = c.getInt(c.getColumnIndex(Mms.THREAD_ID)); - Msg msg = new Msg(id, type); - msgListMms.put(id, msg); - } while (c.moveToNext()); - c.close(); + Msg msg = new Msg(id, type, threadId); + msgListMms.put(id, msg); + } while (c.moveToNext()); + c.close(); + } + synchronized(mMsgListMms) { + mMsgListMms.clear(); + mMsgListMms = msgListMms; + } } - mMsgListMms = msgListMms; + if(mAccount != null) { + HashMap<Long, Msg> msgListEmail = new HashMap<Long, Msg>(); + Uri uri = mMessageUri; + Cursor c = mProviderClient.query(uri, EMAIL_PROJECTION_SHORT, null, null, null); + + if (c != null && c.moveToFirst()) { + do { + long id = c.getLong(c.getColumnIndex(MessageColumns._ID)); + long folderId = c.getInt(c.getColumnIndex(BluetoothMapContract.MessageColumns.FOLDER_ID)); + + Msg msg = new Msg(id, folderId); + msgListEmail.put(id, msg); + } while (c.moveToNext()); + c.close(); + } + synchronized(mMsgListEmail) { + mMsgListEmail.clear(); + mMsgListEmail = msgListEmail; + } + } } private void handleMsgListChangesSms() { @@ -323,34 +517,56 @@ public class BluetoothMapContentObserver { HashMap<Long, Msg> msgListSms = new HashMap<Long, Msg>(); Cursor c = mResolver.query(Sms.CONTENT_URI, - SMS_PROJECTION, null, null, null); + SMS_PROJECTION_SHORT, null, null, null); synchronized(mMsgListSms) { if (c != null && c.moveToFirst()) { do { - long id = c.getLong(c.getColumnIndex(BaseColumns._ID)); + long id = c.getLong(c.getColumnIndex(Sms._ID)); int type = c.getInt(c.getColumnIndex(Sms.TYPE)); + int threadId = c.getInt(c.getColumnIndex(Sms.THREAD_ID)); Msg msg = mMsgListSms.remove(id); + /* We must filter out any actions made by the MCE, hence do not send e.g. a message + * deleted and/or MessageShift for messages deleted by the MCE. */ + if (msg == null) { /* New message */ - msg = new Msg(id, type); + msg = new Msg(id, type, threadId); msgListSms.put(id, msg); - if (folderSms[type].equals("inbox")) { - Event evt = new Event("NewMessage", id, folderSms[type], - null, mSmsType); - sendEvent(evt); - } + /* Incoming message from the network */ + Event evt = new Event(EVENT_TYPE_NEW, id, folderSms[type], + null, mSmsType); + sendEvent(evt); } else { /* Existing message */ if (type != msg.type) { Log.d(TAG, "new type: " + type + " old type: " + msg.type); - Event evt = new Event("MessageShift", id, folderSms[type], - folderSms[msg.type], mSmsType); - sendEvent(evt); + String oldFolder = folderSms[msg.type]; + String newFolder = folderSms[type]; + // Filter out the intermediate outbox steps + if(!oldFolder.equals(newFolder)) { + Event evt = new Event(EVENT_TYPE_SHIFT, id, folderSms[type], + oldFolder, mSmsType); + sendEvent(evt); + } msg.type = type; + } else if(threadId != msg.threadId) { + Log.d(TAG, "Message delete change: type: " + type + " old type: " + msg.type + + "\n threadId: " + threadId + " old threadId: " + msg.threadId); + if(threadId == DELETED_THREAD_ID) { // Message deleted + Event evt = new Event(EVENT_TYPE_DELETE, id, BluetoothMapContract.FOLDER_NAME_DELETED, + folderSms[msg.type], mSmsType); + sendEvent(evt); + msg.threadId = threadId; + } else { // Undelete + Event evt = new Event(EVENT_TYPE_SHIFT, id, folderSms[msg.type], + BluetoothMapContract.FOLDER_NAME_DELETED, mSmsType); + sendEvent(evt); + msg.threadId = threadId; + } } msgListSms.put(id, msg); } @@ -359,8 +575,9 @@ public class BluetoothMapContentObserver { } for (Msg msg : mMsgListSms.values()) { - Event evt = new Event("MessageDeleted", msg.id, "deleted", - folderSms[msg.type], mSmsType); + Event evt = new Event(EVENT_TYPE_DELETE, msg.id, + BluetoothMapContract.FOLDER_NAME_DELETED, + folderSms[msg.type], mSmsType); sendEvent(evt); } @@ -374,46 +591,69 @@ public class BluetoothMapContentObserver { HashMap<Long, Msg> msgListMms = new HashMap<Long, Msg>(); Cursor c = mResolver.query(Mms.CONTENT_URI, - MMS_PROJECTION, null, null, null); + MMS_PROJECTION_SHORT, null, null, null); synchronized(mMsgListMms) { if (c != null && c.moveToFirst()) { do { - long id = c.getLong(c.getColumnIndex(BaseColumns._ID)); + long id = c.getLong(c.getColumnIndex(Mms._ID)); int type = c.getInt(c.getColumnIndex(Mms.MESSAGE_BOX)); int mtype = c.getInt(c.getColumnIndex(Mms.MESSAGE_TYPE)); + int threadId = c.getInt(c.getColumnIndex(Mms.THREAD_ID)); Msg msg = mMsgListMms.remove(id); + /* We must filter out any actions made by the MCE, hence do not send e.g. a message + * deleted and/or MessageShift for messages deleted by the MCE. */ + if (msg == null) { /* New message - only notify on retrieve conf */ - if (folderMms[type].equals("inbox") && + if (folderMms[type].equals(BluetoothMapContract.FOLDER_NAME_INBOX) && mtype != MESSAGE_TYPE_RETRIEVE_CONF) { continue; } - msg = new Msg(id, type); + msg = new Msg(id, type, threadId); msgListMms.put(id, msg); - if (folderMms[type].equals("inbox")) { - Event evt = new Event("NewMessage", id, folderMms[type], + /* Incoming message from the network */ + Event evt = new Event(EVENT_TYPE_NEW, id, folderMms[type], null, TYPE.MMS); - sendEvent(evt); - } + sendEvent(evt); } else { /* Existing message */ if (type != msg.type) { Log.d(TAG, "new type: " + type + " old type: " + msg.type); - Event evt = new Event("MessageShift", id, folderMms[type], - folderMms[msg.type], TYPE.MMS); - sendEvent(evt); + Event evt; + if(msg.localInitiatedSend == false) { + // Only send events about local initiated changes + evt = new Event(EVENT_TYPE_SHIFT, id, folderMms[type], + folderMms[msg.type], TYPE.MMS); + sendEvent(evt); + } msg.type = type; - if (folderMms[type].equals("sent")) { - evt = new Event("SendingSuccess", id, + if (folderMms[type].equals(BluetoothMapContract.FOLDER_NAME_SENT) + && msg.localInitiatedSend == true) { + msg.localInitiatedSend = false; // Stop tracking changes for this message + evt = new Event(EVENT_TYPE_SENDING_SUCCESS, id, folderSms[type], null, TYPE.MMS); sendEvent(evt); } + } else if(threadId != msg.threadId) { + Log.d(TAG, "Message delete change: type: " + type + " old type: " + msg.type + + "\n threadId: " + threadId + " old threadId: " + msg.threadId); + if(threadId == DELETED_THREAD_ID) { // Message deleted + Event evt = new Event(EVENT_TYPE_DELETE, id, BluetoothMapContract.FOLDER_NAME_DELETED, + folderMms[msg.type], TYPE.MMS); + sendEvent(evt); + msg.threadId = threadId; + } else { // Undelete + Event evt = new Event(EVENT_TYPE_SHIFT, id, folderMms[msg.type], + BluetoothMapContract.FOLDER_NAME_DELETED, TYPE.MMS); + sendEvent(evt); + msg.threadId = threadId; + } } msgListMms.put(id, msg); } @@ -422,18 +662,231 @@ public class BluetoothMapContentObserver { } for (Msg msg : mMsgListMms.values()) { - Event evt = new Event("MessageDeleted", msg.id, "deleted", - folderMms[msg.type], TYPE.MMS); + Event evt = new Event(EVENT_TYPE_DELETE, msg.id, + BluetoothMapContract.FOLDER_NAME_DELETED, + folderMms[msg.type], TYPE.MMS); sendEvent(evt); } - mMsgListMms = msgListMms; } } - private void handleMsgListChanges() { - handleMsgListChangesSms(); - handleMsgListChangesMms(); + private void handleMsgListChangesEmail(Uri uri) throws RemoteException{ + if (V) Log.v(TAG, "handleMsgListChangesEmail uri: " + uri.toString()); + + // TODO: Change observer to handle accountId and message ID if present + + HashMap<Long, Msg> msgListEmail = new HashMap<Long, Msg>(); + + Cursor c = mProviderClient.query(mMessageUri, EMAIL_PROJECTION_SHORT, null, null, null); + + synchronized(mMsgListEmail) { + if (c != null && c.moveToFirst()) { + do { + long id = c.getLong(c.getColumnIndex(BluetoothMapContract.MessageColumns._ID)); + int folderId = c.getInt(c.getColumnIndex( + BluetoothMapContract.MessageColumns.FOLDER_ID)); + Msg msg = mMsgListEmail.remove(id); + BluetoothMapFolderElement folderElement = mFolders.getEmailFolderById(folderId); + String newFolder; + if(folderElement != null) { + newFolder = folderElement.getFullPath(); + } else { + newFolder = "unknown"; // This can happen if a new folder is created while connected + } + + /* We must filter out any actions made by the MCE, hence do not send e.g. a message + * deleted and/or MessageShift for messages deleted by the MCE. */ + + if (msg == null) { + /* New message */ + msg = new Msg(id, folderId); + msgListEmail.put(id, msg); + Event evt = new Event(EVENT_TYPE_NEW, id, newFolder, + null, TYPE.EMAIL); + sendEvent(evt); + } else { + /* Existing message */ + if (folderId != msg.folderId) { + if (D) Log.d(TAG, "new folderId: " + folderId + " old folderId: " + msg.folderId); + BluetoothMapFolderElement oldFolderElement = mFolders.getEmailFolderById(msg.folderId); + String oldFolder; + if(oldFolderElement != null) { + oldFolder = oldFolderElement.getFullPath(); + } else { + // This can happen if a new folder is created while connected + oldFolder = "unknown"; + } + BluetoothMapFolderElement deletedFolder = + mFolders.getEmailFolderByName(BluetoothMapContract.FOLDER_NAME_DELETED); + BluetoothMapFolderElement sentFolder = + mFolders.getEmailFolderByName(BluetoothMapContract.FOLDER_NAME_SENT); + /* + * If the folder is now 'deleted', send a deleted-event in stead of a shift + * or if message is sent initiated by MAP Client, then send sending-success + * otherwise send folderShift + */ + if(deletedFolder != null && deletedFolder.getEmailFolderId() == folderId) { + Event evt = new Event(EVENT_TYPE_DELETE, msg.id, newFolder, + oldFolder, TYPE.EMAIL); + sendEvent(evt); + } else if(sentFolder != null + && sentFolder.getEmailFolderId() == folderId + && msg.localInitiatedSend == true) { + if(msg.transparent) { + mResolver.delete(ContentUris.withAppendedId(mMessageUri, id), null, null); + } else { + msg.localInitiatedSend = false; + Event evt = new Event(EVENT_TYPE_SENDING_SUCCESS, msg.id, + oldFolder, null, TYPE.EMAIL); + sendEvent(evt); + } + } else { + Event evt = new Event(EVENT_TYPE_SHIFT, id, newFolder, + oldFolder, TYPE.EMAIL); + sendEvent(evt); + } + msg.folderId = folderId; + } + msgListEmail.put(id, msg); + } + } while (c.moveToNext()); + c.close(); + } + + // For all messages no longer in the database send a delete notification + for (Msg msg : mMsgListEmail.values()) { + BluetoothMapFolderElement oldFolderElement = mFolders.getEmailFolderById(msg.folderId); + String oldFolder; + if(oldFolderElement != null) { + oldFolder = oldFolderElement.getFullPath(); + } else { + oldFolder = "unknown"; + } + /* Some e-mail clients delete the message after sending, and creates a new message in sent. + * We cannot track the message anymore, hence send both a send success and delete message. + */ + if(msg.localInitiatedSend == true) { + msg.localInitiatedSend = false; + // If message is send with transparency don't set folder as message is deleted + if (msg.transparent) + oldFolder = null; + Event evt = new Event(EVENT_TYPE_SENDING_SUCCESS, msg.id, oldFolder, null, TYPE.EMAIL); + sendEvent(evt); + } + /* As this message deleted is only send on a real delete - don't set folder. + * - only send delete event if message is not sent with transparency + */ + if (!msg.transparent) { + + Event evt = new Event(EVENT_TYPE_DELETE, msg.id, null, oldFolder, TYPE.EMAIL); + sendEvent(evt); + } + } + mMsgListEmail = msgListEmail; + } + } + + private void handleMsgListChanges(Uri uri) { + if(uri.getAuthority().equals(mAuthority)) { + try { + handleMsgListChangesEmail(uri); + }catch(RemoteException e){ + mMasInstance.restartObexServerSession(); + Log.w(TAG, "Problems contacting the ContentProvider in mas Instance "+mMasId+" restaring ObexServerSession"); + } + + } else { + handleMsgListChangesSms(); + handleMsgListChangesMms(); + } + } + + private boolean setEmailMessageStatusDelete(BluetoothMapFolderElement mCurrentFolder, + String uriStr, long handle, int status) { + boolean res = false; + Uri uri = Uri.parse(uriStr + BluetoothMapContract.TABLE_MESSAGE); + + int updateCount = 0; + ContentValues contentValues = new ContentValues(); + BluetoothMapFolderElement deleteFolder = mFolders. + getEmailFolderByName(BluetoothMapContract.FOLDER_NAME_DELETED); + contentValues.put(BluetoothMapContract.MessageColumns._ID, handle); + synchronized(mMsgListEmail) { + Msg msg = mMsgListEmail.get(handle); + if (status == BluetoothMapAppParams.STATUS_VALUE_YES) { + /* Set deleted folder id */ + long folderId = -1; + if(deleteFolder != null) { + folderId = deleteFolder.getEmailFolderId(); + } + contentValues.put(BluetoothMapContract.MessageColumns.FOLDER_ID,folderId); + updateCount = mResolver.update(uri, contentValues, null, null); + /* The race between updating the value in our cached values and the database + * is handled by the synchronized statement. */ + if(updateCount > 0) { + res = true; + if (msg != null) { + msg.oldFolderId = msg.folderId; + // Update the folder ID to avoid triggering an event for MCE initiated actions. + msg.folderId = folderId; + } + if(D) Log.d(TAG, "Deleted MSG: " + handle + " from folderId: " + folderId); + } else { + Log.w(TAG, "Msg: " + handle + " - Set delete status " + status + + " failed for folderId " + folderId); + } + } else if (status == BluetoothMapAppParams.STATUS_VALUE_NO) { + /* Undelete message. move to old folder if we know it, + * else move to inbox - as dictated by the spec. */ + if(msg != null && deleteFolder != null && + msg.folderId == deleteFolder.getEmailFolderId()) { + /* Only modify messages in the 'Deleted' folder */ + long folderId = -1; + if (msg != null && msg.oldFolderId != -1) { + folderId = msg.oldFolderId; + } else { + BluetoothMapFolderElement inboxFolder = mCurrentFolder. + getEmailFolderByName(BluetoothMapContract.FOLDER_NAME_INBOX); + if(inboxFolder != null) { + folderId = inboxFolder.getEmailFolderId(); + } + if(D)Log.d(TAG,"We did not delete the message, hence the old folder is unknown. Moving to inbox."); + } + contentValues.put(BluetoothMapContract.MessageColumns.FOLDER_ID, folderId); + updateCount = mResolver.update(uri, contentValues, null, null); + if(updateCount > 0) { + res = true; + // Update the folder ID to avoid triggering an event for MCE initiated actions. + msg.folderId = folderId; + } else { + if(D)Log.d(TAG,"We did not delete the message, hence the old folder is unknown. Moving to inbox."); + } + } + } + if(V) { + BluetoothMapFolderElement folderElement; + String folderName = "unknown"; + if (msg != null) { + folderElement = mCurrentFolder.getEmailFolderById(msg.folderId); + if(folderElement != null) { + folderName = folderElement.getName(); + } + } + Log.d(TAG,"setEmailMessageStatusDelete: " + handle + " from " + folderName + + " status: " + status); + } + } + if(res == false) { + Log.w(TAG, "Set delete status " + status + " failed."); + } + return res; + } + + private void updateThreadId(Uri uri, String valueString, long threadId) { + ContentValues contentValues = new ContentValues(); + contentValues.put(valueString, threadId); + mResolver.update(uri, contentValues, null, null); } private boolean deleteMessageMms(long handle) { @@ -445,12 +898,18 @@ public class BluetoothMapContentObserver { int threadId = c.getInt(c.getColumnIndex(Mms.THREAD_ID)); if (threadId != DELETED_THREAD_ID) { /* Set deleted thread id */ - ContentValues contentValues = new ContentValues(); - contentValues.put(Mms.THREAD_ID, DELETED_THREAD_ID); - mResolver.update(uri, contentValues, null, null); + synchronized(mMsgListMms) { + Msg msg = mMsgListMms.get(handle); + if(msg != null) { // This will always be the case + msg.threadId = DELETED_THREAD_ID; + } + } + updateThreadId(uri, Mms.THREAD_ID, DELETED_THREAD_ID); } else { /* Delete from observer message list to avoid delete notifications */ - mMsgListMms.remove(handle); + synchronized(mMsgListMms) { + mMsgListMms.remove(handle); + } /* Delete message */ mResolver.delete(uri, null, null); } @@ -462,12 +921,6 @@ public class BluetoothMapContentObserver { return res; } - private void updateThreadIdMms(Uri uri, long threadId) { - ContentValues contentValues = new ContentValues(); - contentValues.put(Mms.THREAD_ID, threadId); - mResolver.update(uri, contentValues, null, null); - } - private boolean unDeleteMessageMms(long handle) { boolean res = false; Uri uri = ContentUris.withAppendedId(Mms.CONTENT_URI, handle); @@ -479,7 +932,7 @@ public class BluetoothMapContentObserver { /* Restore thread id from address, or if no thread for address * create new thread by insert and remove of fake message */ String address; - long id = c.getLong(c.getColumnIndex(BaseColumns._ID)); + long id = c.getLong(c.getColumnIndex(Mms._ID)); int msgBox = c.getInt(c.getColumnIndex(Mms.MESSAGE_BOX)); if (msgBox == Mms.MESSAGE_BOX_INBOX) { address = BluetoothMapContent.getAddressMms(mResolver, id, @@ -490,7 +943,14 @@ public class BluetoothMapContentObserver { } Set<String> recipients = new HashSet<String>(); recipients.addAll(Arrays.asList(address)); - updateThreadIdMms(uri, Telephony.Threads.getOrCreateThreadId(mContext, recipients)); + Long oldThreadId = Telephony.Threads.getOrCreateThreadId(mContext, recipients); + synchronized(mMsgListMms) { + Msg msg = mMsgListMms.get(handle); + if(msg != null) { // This will always be the case + msg.threadId = oldThreadId.intValue(); + } + } + updateThreadId(uri, Mms.THREAD_ID, oldThreadId); } else { Log.d(TAG, "Message not in deleted folder: handle " + handle + " threadId " + threadId); @@ -512,13 +972,19 @@ public class BluetoothMapContentObserver { /* Move to deleted folder, or delete if already in deleted folder */ int threadId = c.getInt(c.getColumnIndex(Sms.THREAD_ID)); if (threadId != DELETED_THREAD_ID) { + synchronized(mMsgListSms) { + Msg msg = mMsgListSms.get(handle); + if(msg != null) { // This will always be the case + msg.threadId = DELETED_THREAD_ID; + } + } /* Set deleted thread id */ - ContentValues contentValues = new ContentValues(); - contentValues.put(Sms.THREAD_ID, DELETED_THREAD_ID); - mResolver.update(uri, contentValues, null, null); + updateThreadId(uri, Sms.THREAD_ID, DELETED_THREAD_ID); } else { /* Delete from observer message list to avoid delete notifications */ - mMsgListSms.remove(handle); + synchronized(mMsgListSms) { + mMsgListSms.remove(handle); + } /* Delete message */ mResolver.delete(uri, null, null); } @@ -530,12 +996,6 @@ public class BluetoothMapContentObserver { return res; } - private void updateThreadIdSms(Uri uri, long threadId) { - ContentValues contentValues = new ContentValues(); - contentValues.put(Sms.THREAD_ID, threadId); - mResolver.update(uri, contentValues, null, null); - } - private boolean unDeleteMessageSms(long handle) { boolean res = false; Uri uri = ContentUris.withAppendedId(Sms.CONTENT_URI, handle); @@ -547,7 +1007,14 @@ public class BluetoothMapContentObserver { String address = c.getString(c.getColumnIndex(Sms.ADDRESS)); Set<String> recipients = new HashSet<String>(); recipients.addAll(Arrays.asList(address)); - updateThreadIdSms(uri, Telephony.Threads.getOrCreateThreadId(mContext, recipients)); + Long oldThreadId = Telephony.Threads.getOrCreateThreadId(mContext, recipients); + synchronized(mMsgListSms) { + Msg msg = mMsgListSms.get(handle); + if(msg != null) { // This will always be the case + msg.threadId = oldThreadId.intValue(); // The threadId is specified as an int, so it is safe to truncate + } + } + updateThreadId(uri, Sms.THREAD_ID, oldThreadId); } else { Log.d(TAG, "Message not in deleted folder: handle " + handle + " threadId " + threadId); @@ -560,29 +1027,43 @@ public class BluetoothMapContentObserver { return res; } - public boolean setMessageStatusDeleted(long handle, TYPE type, int statusValue) { + public boolean setMessageStatusDeleted(long handle, TYPE type, + BluetoothMapFolderElement mCurrentFolder, String uriStr, int statusValue) { boolean res = false; if (D) Log.d(TAG, "setMessageStatusDeleted: handle " + handle + " type " + type + " value " + statusValue); - if (statusValue == BluetoothMapAppParams.STATUS_VALUE_YES) { - if (type == TYPE.SMS_GSM || type == TYPE.SMS_CDMA) { - res = deleteMessageSms(handle); - } else if (type == TYPE.MMS) { - res = deleteMessageMms(handle); - } - } else if (statusValue == BluetoothMapAppParams.STATUS_VALUE_NO) { - if (type == TYPE.SMS_GSM || type == TYPE.SMS_CDMA) { - res = unDeleteMessageSms(handle); - } else if (type == TYPE.MMS) { - res = unDeleteMessageMms(handle); + if (type == TYPE.EMAIL) { + res = setEmailMessageStatusDelete(mCurrentFolder, uriStr, handle, statusValue); + } else { + if (statusValue == BluetoothMapAppParams.STATUS_VALUE_YES) { + if (type == TYPE.SMS_GSM || type == TYPE.SMS_CDMA) { + res = deleteMessageSms(handle); + } else if (type == TYPE.MMS) { + res = deleteMessageMms(handle); + } + } else if (statusValue == BluetoothMapAppParams.STATUS_VALUE_NO) { + if (type == TYPE.SMS_GSM || type == TYPE.SMS_CDMA) { + res = unDeleteMessageSms(handle); + } else if (type == TYPE.MMS) { + res = unDeleteMessageMms(handle); + } } } + return res; } - public boolean setMessageStatusRead(long handle, TYPE type, int statusValue) { - boolean res = true; + /** + * + * @param handle + * @param type + * @param uriStr + * @param statusValue + * @return true at success + */ + public boolean setMessageStatusRead(long handle, TYPE type, String uriStr, int statusValue) throws RemoteException{ + int count = 0; if (D) Log.d(TAG, "setMessageStatusRead: handle " + handle + " type " + type + " value " + statusValue); @@ -591,22 +1072,36 @@ public class BluetoothMapContentObserver { /* by the MCE shall change the MSE read status. */ if (type == TYPE.SMS_GSM || type == TYPE.SMS_CDMA) { - Uri uri = ContentUris.withAppendedId(Sms.CONTENT_URI, handle); + Uri uri = Sms.Inbox.CONTENT_URI;//ContentUris.withAppendedId(Sms.CONTENT_URI, handle); Cursor c = mResolver.query(uri, null, null, null, null); - ContentValues contentValues = new ContentValues(); contentValues.put(Sms.READ, statusValue); - mResolver.update(uri, contentValues, null, null); + contentValues.put(Sms.SEEN, statusValue); + String where = Sms._ID+"="+handle; + String values = contentValues.toString(); + if (D) Log.d(TAG, " -> SMS Uri: " + uri.toString() + " Where " + where + " values " + values); + count = mResolver.update(uri, contentValues, where, null); + if (D) Log.d(TAG, " -> "+count +" rows updated!"); } else if (type == TYPE.MMS) { Uri uri = ContentUris.withAppendedId(Mms.CONTENT_URI, handle); Cursor c = mResolver.query(uri, null, null, null, null); - + if (D) Log.d(TAG, " -> MMS Uri: " + uri.toString()); ContentValues contentValues = new ContentValues(); contentValues.put(Mms.READ, statusValue); - mResolver.update(uri, contentValues, null, null); - } + count = mResolver.update(uri, contentValues, null, null); - return res; + if (D) Log.d(TAG, " -> "+count +" rows updated!"); + } if (type == TYPE.EMAIL) { + Uri uri = mMessageUri; + ContentValues contentValues = new ContentValues(); + contentValues.put(BluetoothMapContract.MessageColumns.FLAG_READ, statusValue); + contentValues.put(BluetoothMapContract.MessageColumns._ID, handle); + count = mProviderClient.update(uri, contentValues, null, null); + } + if(count < 1) { + return false; + } + return true; } private class PushMsgInfo { @@ -615,10 +1110,14 @@ public class BluetoothMapContentObserver { int retry; String phone; Uri uri; + long timestamp; int parts; int partsSent; int partsDelivered; boolean resend; + boolean sendInProgress; + boolean failedSent; // Set to true if a single part sent fail is received. + int statusDelivered; // Set to != 0 if a single part deliver fail is received. public PushMsgInfo(long id, int transparent, int retry, String phone, Uri uri) { @@ -628,14 +1127,19 @@ public class BluetoothMapContentObserver { this.phone = phone; this.uri = uri; this.resend = false; + this.sendInProgress = false; + this.failedSent = false; + this.statusDelivered = 0; /* Assume success */ + this.timestamp = 0; }; } private Map<Long, PushMsgInfo> mPushMsgList = Collections.synchronizedMap(new HashMap<Long, PushMsgInfo>()); - public long pushMessage(BluetoothMapbMessage msg, String folder, - BluetoothMapAppParams ap) throws IllegalArgumentException { + public long pushMessage(BluetoothMapbMessage msg, BluetoothMapFolderElement folderElement, + BluetoothMapAppParams ap, String emailBaseUri) + throws IllegalArgumentException, RemoteException, IOException { if (D) Log.d(TAG, "pushMessage"); ArrayList<BluetoothMapbMessage.vCard> recipientList = msg.getRecipients(); int transparent = (ap.getTransparent() == BluetoothMapAppParams.INVALID_VALUE_PARAMETER) ? @@ -643,61 +1147,158 @@ public class BluetoothMapContentObserver { int retry = ap.getRetry(); int charset = ap.getCharset(); long handle = -1; + long folderId = -1; if (recipientList == null) { - Log.d(TAG, "empty recipient list"); + if (D) Log.d(TAG, "empty recipient list"); return -1; } - for (BluetoothMapbMessage.vCard recipient : recipientList) { - if(recipient.getEnvLevel() == 0) // Only send the message to the top level recipient - { - /* Only send to first address */ - String phone = recipient.getFirstPhoneNumber(); - boolean read = false; - boolean deliveryReport = true; - - switch(msg.getType()){ - case MMS: - { - /* Send message if folder is outbox */ - /* to do, support MMS in the future */ - /* - handle = sendMmsMessage(folder, phone, (BluetoothMapbMessageMmsEmail)msg); - */ - break; + if ( msg.getType().equals(TYPE.EMAIL) ) { + /* Write the message to the database */ + String msgBody = ((BluetoothMapbMessageEmail) msg).getEmailBody(); + if (V) { + int length = msgBody.length(); + Log.v(TAG, "pushMessage: message string length = " + length); + String messages[] = msgBody.split("\r\n"); + Log.v(TAG, "pushMessage: messages count=" + messages.length); + for(int i = 0; i < messages.length; i++) { + Log.v(TAG, "part " + i + ":" + messages[i]); + } + } + FileOutputStream os = null; + ParcelFileDescriptor fdOut = null; + Uri uriInsert = Uri.parse(emailBaseUri + BluetoothMapContract.TABLE_MESSAGE); + if (D) Log.d(TAG, "pushMessage - uriInsert= " + uriInsert.toString() + + ", intoFolder id=" + folderElement.getEmailFolderId()); + + synchronized(mMsgListEmail) { + // Now insert the empty message into folder + ContentValues values = new ContentValues(); + folderId = folderElement.getEmailFolderId(); + values.put(BluetoothMapContract.MessageColumns.FOLDER_ID, folderId); + Uri uriNew = mProviderClient.insert(uriInsert, values); + if (D) Log.d(TAG, "pushMessage - uriNew= " + uriNew.toString()); + handle = Long.parseLong(uriNew.getLastPathSegment()); + + try { + fdOut = mProviderClient.openFile(uriNew, "w"); + os = new FileOutputStream(fdOut.getFileDescriptor()); + // Write Email to DB + os.write(msgBody.getBytes(), 0, msgBody.getBytes().length); + } catch (FileNotFoundException e) { + Log.w(TAG, e); + throw(new IOException("Unable to open file stream")); + } catch (NullPointerException e) { + Log.w(TAG, e); + throw(new IllegalArgumentException("Unable to parse message.")); + } finally { + try { + if(os != null) + os.close(); + } catch (IOException e) {Log.w(TAG, e);} + try { + if(fdOut != null) + fdOut.close(); + } catch (IOException e) {Log.w(TAG, e);} + } + + /* Extract the data for the inserted message, and store in local mirror, to + * avoid sending a NewMessage Event. */ + Msg newMsg = new Msg(handle, folderId); + newMsg.transparent = (transparent == 1) ? true : false; + if ( folderId == folderElement.getEmailFolderByName( + BluetoothMapContract.FOLDER_NAME_OUTBOX).getEmailFolderId() ) { + newMsg.localInitiatedSend = true; + } + mMsgListEmail.put(handle, newMsg); + } + } else { // type SMS_* of MMS + for (BluetoothMapbMessage.vCard recipient : recipientList) { + if(recipient.getEnvLevel() == 0) // Only send the message to the top level recipient + { + /* Only send to first address */ + String phone = recipient.getFirstPhoneNumber(); + String email = recipient.getFirstEmail(); + String folder = folderElement.getName(); + boolean read = false; + boolean deliveryReport = true; + String msgBody = null; + + /* If MMS contains text only and the size is less than ten SMS's + * then convert the MMS to type SMS and then proceed + */ + if (msg.getType().equals(TYPE.MMS) && + (((BluetoothMapbMessageMms) msg).getTextOnly() == true)) { + msgBody = ((BluetoothMapbMessageMms) msg).getMessageAsText(); + SmsManager smsMng = SmsManager.getDefault(); + ArrayList<String> parts = smsMng.divideMessage(msgBody); + int smsParts = parts.size(); + if (smsParts <= CONVERT_MMS_TO_SMS_PART_COUNT ) { + if (D) Log.d(TAG, "pushMessage - converting MMS to SMS, sms parts=" + smsParts ); + msg.setType(mSmsType); + } else { + if (D) Log.d(TAG, "pushMessage - MMS text only but to big to convert to SMS"); + msgBody = null; + } + } - case SMS_GSM: //fall-through - case SMS_CDMA: - { + + if (msg.getType().equals(TYPE.MMS)) { + /* Send message if folder is outbox else just store in draft*/ + handle = sendMmsMessage(folder, phone, (BluetoothMapbMessageMms)msg); + } else if (msg.getType().equals(TYPE.SMS_GSM) || + msg.getType().equals(TYPE.SMS_CDMA) ) { /* Add the message to the database */ - String msgBody = ((BluetoothMapbMessageSms) msg).getSmsBody(); - Uri contentUri = Uri.parse("content://sms/" + folder); - Uri uri = Sms.addMessageToUri(mResolver, contentUri, phone, msgBody, - "", System.currentTimeMillis(), read, deliveryReport); - - if (uri == null) { - Log.d(TAG, "pushMessage - failure on add to uri " + contentUri); - return -1; - } + if(msgBody == null) + msgBody = ((BluetoothMapbMessageSms) msg).getSmsBody(); + + /* We need to lock the SMS list while updating the database, to avoid sending + * events on MCE initiated operation. */ + Uri contentUri = Uri.parse(Sms.CONTENT_URI+ "/" + folder); + Uri uri; + synchronized(mMsgListSms) { + uri = Sms.addMessageToUri(mResolver, contentUri, phone, msgBody, + "", System.currentTimeMillis(), read, deliveryReport); + + if(V) Log.v(TAG, "Sms.addMessageToUri() returned: " + uri); + if (uri == null) { + if (D) Log.d(TAG, "pushMessage - failure on add to uri " + contentUri); + return -1; + } + Cursor c = mResolver.query(uri, SMS_PROJECTION_SHORT, null, null, null); + + /* Extract the data for the inserted message, and store in local mirror, to + * avoid sending a NewMessage Event. */ + if (c != null && c.moveToFirst()) { + long id = c.getLong(c.getColumnIndex(Sms._ID)); + int type = c.getInt(c.getColumnIndex(Sms.TYPE)); + int threadId = c.getInt(c.getColumnIndex(Sms.THREAD_ID)); + Msg newMsg = new Msg(id, type, threadId); + mMsgListSms.put(id, newMsg); + c.close(); + } else { + return -1; // This can only happen, if the message is deleted just as it is added + } - handle = Long.parseLong(uri.getLastPathSegment()); + handle = Long.parseLong(uri.getLastPathSegment()); - /* Send message if folder is outbox */ - if (folder.equals("outbox")) { - PushMsgInfo msgInfo = new PushMsgInfo(handle, transparent, - retry, phone, uri); - mPushMsgList.put(handle, msgInfo); - sendMessage(msgInfo, msgBody); + /* Send message if folder is outbox */ + if (folder.equals(BluetoothMapContract.FOLDER_NAME_OUTBOX)) { + PushMsgInfo msgInfo = new PushMsgInfo(handle, transparent, + retry, phone, uri); + mPushMsgList.put(handle, msgInfo); + sendMessage(msgInfo, msgBody); + if(V) Log.v(TAG, "sendMessage returned..."); + } + /* sendMessage causes the message to be deleted and reinserted, hence we need to lock + * the list while this is happening. */ } - break; - } - case EMAIL: - { - break; + } else { + if (D) Log.d(TAG, "pushMessage - failure on type " ); + return -1; } } - } } @@ -705,9 +1306,7 @@ public class BluetoothMapContentObserver { return handle; } - - - public long sendMmsMessage(String folder,String to_address, BluetoothMapbMessageMmsEmail msg) { + public long sendMmsMessage(String folder, String to_address, BluetoothMapbMessageMms msg) { /* *strategy: *1) parse message into parts @@ -719,42 +1318,46 @@ public class BluetoothMapContentObserver { *else if folder !outbox: *1) push message to folder * */ - long handle = pushMmsToFolder(Mms.MESSAGE_BOX_DRAFTS, to_address, msg); - /* if invalid handle (-1) then just return the handle - else continue sending (if folder is outbox) */ - if (BluetoothMapAppParams.INVALID_VALUE_PARAMETER != handle && folder.equalsIgnoreCase("outbox")) { - moveDraftToOutbox(handle); - - Intent sendIntent = new Intent("android.intent.action.MMS_SEND_OUTBOX_MSG"); - Log.d(TAG, "broadcasting intent: "+sendIntent.toString()); - mContext.sendBroadcast(sendIntent); + if (folder != null && (folder.equalsIgnoreCase(BluetoothMapContract.FOLDER_NAME_OUTBOX) + || folder.equalsIgnoreCase(BluetoothMapContract.FOLDER_NAME_DRAFT))) { + long handle = pushMmsToFolder(Mms.MESSAGE_BOX_DRAFTS, to_address, msg); + /* if invalid handle (-1) then just return the handle - else continue sending (if folder is outbox) */ + if (BluetoothMapAppParams.INVALID_VALUE_PARAMETER != handle && folder.equalsIgnoreCase(BluetoothMapContract.FOLDER_NAME_OUTBOX)) { + moveDraftToOutbox(handle); + Intent sendIntent = new Intent("android.intent.action.MMS_SEND_OUTBOX_MSG"); + if (D) Log.d(TAG, "broadcasting intent: "+sendIntent.toString()); + mContext.sendBroadcast(sendIntent); + } + return handle; + } else { + /* not allowed to push mms to anything but outbox/draft */ + throw new IllegalArgumentException("Cannot push message to other folders than outbox/draft"); } - return handle; } private void moveDraftToOutbox(long handle) { - ContentResolver contentResolver = mContext.getContentResolver(); /*Move message by changing the msg_box value in the content provider database */ if (handle != -1) { String whereClause = " _id= " + handle; - Uri uri = Uri.parse("content://mms"); - Cursor queryResult = contentResolver.query(uri, null, whereClause, null, null); + Uri uri = Mms.CONTENT_URI; + Cursor queryResult = mResolver.query(uri, null, whereClause, null, null); if (queryResult != null) { if (queryResult.getCount() > 0) { queryResult.moveToFirst(); ContentValues data = new ContentValues(); /* set folder to be outbox */ - data.put("msg_box", Mms.MESSAGE_BOX_OUTBOX); - contentResolver.update(uri, data, whereClause, null); - Log.d(TAG, "moved draft MMS to outbox"); + data.put(Mms.MESSAGE_BOX, Mms.MESSAGE_BOX_OUTBOX); + mResolver.update(uri, data, whereClause, null); + if (D) Log.d(TAG, "moved draft MMS to outbox"); } queryResult.close(); }else { - Log.d(TAG, "Could not move draft to outbox "); + if (D) Log.d(TAG, "Could not move draft to outbox "); } } } - private long pushMmsToFolder(int folder, String to_address, BluetoothMapbMessageMmsEmail msg) { + private long pushMmsToFolder(int folder, String to_address, BluetoothMapbMessageMms msg) { /** * strategy: * 1) parse msg into parts + header @@ -764,101 +1367,153 @@ public class BluetoothMapContentObserver { */ ContentValues values = new ContentValues(); - values.put("msg_box", folder); - - values.put("read", 0); - values.put("seen", 0); - values.put("sub", msg.getSubject()); - values.put("sub_cs", 106); - values.put("ct_t", "application/vnd.wap.multipart.related"); - values.put("exp", 604800); - values.put("m_cls", PduHeaders.MESSAGE_CLASS_PERSONAL_STR); - values.put("m_type", PduHeaders.MESSAGE_TYPE_SEND_REQ); - values.put("v", PduHeaders.CURRENT_MMS_VERSION); - values.put("pri", PduHeaders.PRIORITY_NORMAL); - values.put("rr", PduHeaders.VALUE_NO); - values.put("tr_id", "T"+ Long.toHexString(System.currentTimeMillis())); - values.put("d_rpt", PduHeaders.VALUE_NO); - values.put("locked", 0); - if(msg.getTextOnly() == true) - values.put("text_only", true); + values.put(Mms.MESSAGE_BOX, folder); + values.put(Mms.READ, 0); + values.put(Mms.SEEN, 0); + if(msg.getSubject() != null) { + values.put(Mms.SUBJECT, msg.getSubject()); + } else { + values.put(Mms.SUBJECT, ""); + } - values.put("m_size", msg.getSize()); + if(msg.getSubject() != null && msg.getSubject().length() > 0) { + values.put(Mms.SUBJECT_CHARSET, 106); + } + values.put(Mms.CONTENT_TYPE, "application/vnd.wap.multipart.related"); + values.put(Mms.EXPIRY, 604800); + values.put(Mms.MESSAGE_CLASS, PduHeaders.MESSAGE_CLASS_PERSONAL_STR); + values.put(Mms.MESSAGE_TYPE, PduHeaders.MESSAGE_TYPE_SEND_REQ); + values.put(Mms.MMS_VERSION, PduHeaders.CURRENT_MMS_VERSION); + values.put(Mms.PRIORITY, PduHeaders.PRIORITY_NORMAL); + values.put(Mms.READ_REPORT, PduHeaders.VALUE_NO); + values.put(Mms.TRANSACTION_ID, "T"+ Long.toHexString(System.currentTimeMillis())); + values.put(Mms.DELIVERY_REPORT, PduHeaders.VALUE_NO); + values.put(Mms.LOCKED, 0); + if(msg.getTextOnly() == true) + values.put(Mms.TEXT_ONLY, true); + values.put(Mms.MESSAGE_SIZE, msg.getSize()); - // Get thread id + // Get thread id Set<String> recipients = new HashSet<String>(); recipients.addAll(Arrays.asList(to_address)); - values.put("thread_id", Telephony.Threads.getOrCreateThreadId(mContext, recipients)); - Uri uri = Uri.parse("content://mms"); + values.put(Mms.THREAD_ID, Telephony.Threads.getOrCreateThreadId(mContext, recipients)); + Uri uri = Mms.CONTENT_URI; - ContentResolver cr = mContext.getContentResolver(); - uri = cr.insert(uri, values); + synchronized (mMsgListMms) { - if (uri == null) { - // unable to insert MMS - Log.e(TAG, "Unabled to insert MMS " + values + "Uri: " + uri); - return -1; - } + uri = mResolver.insert(uri, values); + + if (uri == null) { + // unable to insert MMS + Log.e(TAG, "Unabled to insert MMS " + values + "Uri: " + uri); + return -1; + } + /* As we already have all the values we need, we could skip the query, but + doing the query ensures we get any changes made by the content provider + at insert. */ + Cursor c = mResolver.query(uri, MMS_PROJECTION_SHORT, null, null, null); + + if (c != null && c.moveToFirst()) { + long id = c.getLong(c.getColumnIndex(Mms._ID)); + int type = c.getInt(c.getColumnIndex(Mms.MESSAGE_BOX)); + int threadId = c.getInt(c.getColumnIndex(Mms.THREAD_ID)); + + /* We must filter out any actions made by the MCE. Add the new message to + * the list of known messages. */ + + Msg newMsg = new Msg(id, type, threadId); + newMsg.localInitiatedSend = true; + mMsgListMms.put(id, newMsg); + c.close(); + } + } // Done adding changes, unlock access to mMsgListMms to allow sending MMS events again long handle = Long.parseLong(uri.getLastPathSegment()); - if (V){ - Log.v(TAG, " NEW URI " + uri.toString()); - } + if (V) Log.v(TAG, " NEW URI " + uri.toString()); + try { - if(V) Log.v(TAG, "Adding " + msg.getMimeParts().size() + " parts to the data base."); - for(MimePart part : msg.getMimeParts()) { - int count = 0; - count++; - values.clear(); - if(part.contentType != null && part.contentType.toUpperCase().contains("TEXT")) { - values.put("ct", "text/plain"); - values.put("chset", 106); - if(part.partName != null) { - values.put("fn", part.partName); - values.put("name", part.partName); - } else if(part.contentId == null && part.contentLocation == null) { - /* We must set at least one part identifier */ - values.put("fn", "text_" + count +".txt"); - values.put("name", "text_" + count +".txt"); - } - if(part.contentId != null) { - values.put("cid", part.contentId); + if(msg.getMimeParts() == null) { + /* Perhaps this message have been deleted, and no longer have any content, but only headers */ + Log.w(TAG, "No MMS parts present..."); + } else { + if(V) Log.v(TAG, "Adding " + msg.getMimeParts().size() + " parts to the data base."); + for(MimePart part : msg.getMimeParts()) { + int count = 0; + count++; + values.clear(); + if(part.mContentType != null && part.mContentType.toUpperCase().contains("TEXT")) { + values.put(Mms.Part.CONTENT_TYPE, "text/plain"); + values.put(Mms.Part.CHARSET, 106); + if(part.mPartName != null) { + values.put(Mms.Part.FILENAME, part.mPartName); + values.put(Mms.Part.NAME, part.mPartName); + } else { + values.put(Mms.Part.FILENAME, "text_" + count +".txt"); + values.put(Mms.Part.NAME, "text_" + count +".txt"); + } + // Ensure we have "ci" set + if(part.mContentId != null) { + values.put(Mms.Part.CONTENT_ID, part.mContentId); + } else { + if(part.mPartName != null) { + values.put(Mms.Part.CONTENT_ID, "<" + part.mPartName + ">"); + } else { + values.put(Mms.Part.CONTENT_ID, "<text_" + count + ">"); + } + } + // Ensure we have "cl" set + if(part.mContentLocation != null) { + values.put(Mms.Part.CONTENT_LOCATION, part.mContentLocation); + } else { + if(part.mPartName != null) { + values.put(Mms.Part.CONTENT_LOCATION, part.mPartName + ".txt"); + } else { + values.put(Mms.Part.CONTENT_LOCATION, "text_" + count + ".txt"); + } + } + + if(part.mContentDisposition != null) { + values.put(Mms.Part.CONTENT_DISPOSITION, part.mContentDisposition); + } + values.put(Mms.Part.TEXT, part.getDataAsString()); + uri = Uri.parse(Mms.CONTENT_URI + "/" + handle + "/part"); + uri = mResolver.insert(uri, values); + if(V) Log.v(TAG, "Added TEXT part"); + + } else if (part.mContentType != null && part.mContentType.toUpperCase().contains("SMIL")){ + + values.put(Mms.Part.SEQ, -1); + values.put(Mms.Part.CONTENT_TYPE, "application/smil"); + if(part.mContentId != null) { + values.put(Mms.Part.CONTENT_ID, part.mContentId); + } else { + values.put(Mms.Part.CONTENT_ID, "<smil_" + count + ">"); + } + if(part.mContentLocation != null) { + values.put(Mms.Part.CONTENT_LOCATION, part.mContentLocation); + } else { + values.put(Mms.Part.CONTENT_LOCATION, "smil_" + count + ".xml"); + } + + if(part.mContentDisposition != null) + values.put(Mms.Part.CONTENT_DISPOSITION, part.mContentDisposition); + values.put(Mms.Part.FILENAME, "smil.xml"); + values.put(Mms.Part.NAME, "smil.xml"); + values.put(Mms.Part.TEXT, new String(part.mData, "UTF-8")); + + uri = Uri.parse(Mms.CONTENT_URI+ "/" + handle + "/part"); + uri = mResolver.insert(uri, values); + if (V) Log.v(TAG, "Added SMIL part"); + + }else /*VIDEO/AUDIO/IMAGE*/ { + writeMmsDataPart(handle, part, count); + if (V) Log.v(TAG, "Added OTHER part"); + } + if (uri != null){ + if (V) Log.v(TAG, "Added part with content-type: "+ part.mContentType + " to Uri: " + uri.toString()); + } } - if(part.contentLocation != null) - values.put("cl", part.contentLocation); - if(part.contentDisposition != null) - values.put("cd", part.contentDisposition); - values.put("text", new String(part.data, "UTF-8")); - uri = Uri.parse("content://mms/" + handle + "/part"); - uri = cr.insert(uri, values); - if(V) Log.v(TAG, "Added TEXT part"); - - } else if (part.contentType != null && part.contentType.toUpperCase().contains("SMIL")){ - - values.put("seq", -1); - values.put("ct", "application/smil"); - if(part.contentId != null) - values.put("cid", part.contentId); - if(part.contentLocation != null) - values.put("cl", part.contentLocation); - if(part.contentDisposition != null) - values.put("cd", part.contentDisposition); - values.put("fn", "smil.xml"); - values.put("name", "smil.xml"); - values.put("text", new String(part.data, "UTF-8")); - - uri = Uri.parse("content://mms/" + handle + "/part"); - uri = cr.insert(uri, values); - if(V) Log.v(TAG, "Added SMIL part"); - - }else /*VIDEO/AUDIO/IMAGE*/ { - writeMmsDataPart(handle, part, count); - if(V) Log.v(TAG, "Added OTHER part"); - } - if (uri != null && V){ - Log.v(TAG, "Added part with content-type: "+ part.contentType + " to Uri: " + uri.toString()); } - } } catch (UnsupportedEncodingException e) { Log.w(TAG, e); } catch (IOException e) { @@ -866,25 +1521,25 @@ public class BluetoothMapContentObserver { } values.clear(); - values.put("contact_id", "null"); - values.put("address", "insert-address-token"); - values.put("type", BluetoothMapContent.MMS_FROM); - values.put("charset", 106); + values.put(Mms.Addr.CONTACT_ID, "null"); + values.put(Mms.Addr.ADDRESS, "insert-address-token"); + values.put(Mms.Addr.TYPE, BluetoothMapContent.MMS_FROM); + values.put(Mms.Addr.CHARSET, 106); - uri = Uri.parse("content://mms/" + handle + "/addr"); - uri = cr.insert(uri, values); + uri = Uri.parse(Mms.CONTENT_URI + "/" + handle + "/addr"); + uri = mResolver.insert(uri, values); if (uri != null && V){ Log.v(TAG, " NEW URI " + uri.toString()); } values.clear(); - values.put("contact_id", "null"); - values.put("address", to_address); - values.put("type", BluetoothMapContent.MMS_TO); - values.put("charset", 106); + values.put(Mms.Addr.CONTACT_ID, "null"); + values.put(Mms.Addr.ADDRESS, to_address); + values.put(Mms.Addr.TYPE, BluetoothMapContent.MMS_TO); + values.put(Mms.Addr.CHARSET, 106); - uri = Uri.parse("content://mms/" + handle + "/addr"); - uri = cr.insert(uri, values); + uri = Uri.parse(Mms.CONTENT_URI + "/" + handle + "/addr"); + uri = mResolver.insert(uri, values); if (uri != null && V){ Log.v(TAG, " NEW URI " + uri.toString()); } @@ -894,34 +1549,47 @@ public class BluetoothMapContentObserver { private void writeMmsDataPart(long handle, MimePart part, int count) throws IOException{ ContentValues values = new ContentValues(); - values.put("mid", handle); - if(part.contentType != null){ - //Remove last char if ';' from contentType - if(part.contentType.charAt(part.contentType.length() - 1) == ';') { - part.contentType = part.contentType.substring(0,part.contentType.length() -1); + values.put(Mms.Part.MSG_ID, handle); + if(part.mContentType != null) { + values.put(Mms.Part.CONTENT_TYPE, part.mContentType); + } else { + Log.w(TAG, "MMS has no CONTENT_TYPE for part " + count); + } + if(part.mContentId != null) { + values.put(Mms.Part.CONTENT_ID, part.mContentId); + } else { + if(part.mPartName != null) { + values.put(Mms.Part.CONTENT_ID, "<" + part.mPartName + ">"); + } else { + values.put(Mms.Part.CONTENT_ID, "<part_" + count + ">"); } - values.put("ct", part.contentType); } - if(part.contentId != null) - values.put("cid", part.contentId); - if(part.contentLocation != null) - values.put("cl", part.contentLocation); - if(part.contentDisposition != null) - values.put("cd", part.contentDisposition); - if(part.partName != null) { - values.put("fn", part.partName); - values.put("name", part.partName); - } else if(part.contentId == null && part.contentLocation == null) { + + if(part.mContentLocation != null) { + values.put(Mms.Part.CONTENT_LOCATION, part.mContentLocation); + } else { + if(part.mPartName != null) { + values.put(Mms.Part.CONTENT_LOCATION, part.mPartName + ".dat"); + } else { + values.put(Mms.Part.CONTENT_LOCATION, "part_" + count + ".dat"); + } + } + if(part.mContentDisposition != null) + values.put(Mms.Part.CONTENT_DISPOSITION, part.mContentDisposition); + if(part.mPartName != null) { + values.put(Mms.Part.FILENAME, part.mPartName); + values.put(Mms.Part.NAME, part.mPartName); + } else { /* We must set at least one part identifier */ - values.put("fn", "part_" + count + ".dat"); - values.put("name", "part_" + count + ".dat"); + values.put(Mms.Part.FILENAME, "part_" + count + ".dat"); + values.put(Mms.Part.NAME, "part_" + count + ".dat"); } - Uri partUri = Uri.parse("content://mms/" + handle + "/part"); + Uri partUri = Uri.parse(Mms.CONTENT_URI + "/" + handle + "/part"); Uri res = mResolver.insert(partUri, values); // Add data to part OutputStream os = mResolver.openOutputStream(res); - os.write(part.data); + os.write(part.mData); os.close(); } @@ -931,21 +1599,52 @@ public class BluetoothMapContentObserver { SmsManager smsMng = SmsManager.getDefault(); ArrayList<String> parts = smsMng.divideMessage(msgBody); msgInfo.parts = parts.size(); + // We add a time stamp to differentiate delivery reports from each other for resent messages + msgInfo.timestamp = Calendar.getInstance().getTime().getTime(); + msgInfo.partsDelivered = 0; + msgInfo.partsSent = 0; ArrayList<PendingIntent> deliveryIntents = new ArrayList<PendingIntent>(msgInfo.parts); ArrayList<PendingIntent> sentIntents = new ArrayList<PendingIntent>(msgInfo.parts); + /* We handle the SENT intent in the MAP service, as this object + * is destroyed at disconnect, hence if a disconnect occur while sending + * a message, there is no intent handler to move the message from outbox + * to the correct folder. + * The correct solution would be to create a service that will start based on + * the intent, if BT is turned off. */ + for (int i = 0; i < msgInfo.parts; i++) { - Intent intent; - intent = new Intent(ACTION_MESSAGE_DELIVERY, null); - intent.putExtra("HANDLE", msgInfo.id); - deliveryIntents.add(PendingIntent.getBroadcast(mContext, 0, intent, - PendingIntent.FLAG_UPDATE_CURRENT)); - - intent = new Intent(ACTION_MESSAGE_SENT, null); - intent.putExtra("HANDLE", msgInfo.id); - sentIntents.add(PendingIntent.getBroadcast(mContext, 0, intent, - PendingIntent.FLAG_UPDATE_CURRENT)); + Intent intentDelivery, intentSent; + + intentDelivery = new Intent(ACTION_MESSAGE_DELIVERY, null); + /* Add msgId and part number to ensure the intents are different, and we + * thereby get an intent for each msg part. + * setType is needed to create different intents for each message id/ time stamp, + * as the extras are not used when comparing. */ + intentDelivery.setType("message/" + Long.toString(msgInfo.id) + msgInfo.timestamp + i); + intentDelivery.putExtra(EXTRA_MESSAGE_SENT_HANDLE, msgInfo.id); + intentDelivery.putExtra(EXTRA_MESSAGE_SENT_TIMESTAMP, msgInfo.timestamp); + PendingIntent pendingIntentDelivery = PendingIntent.getBroadcast(mContext, 0, + intentDelivery, PendingIntent.FLAG_UPDATE_CURRENT); + + intentSent = new Intent(ACTION_MESSAGE_SENT, null); + /* Add msgId and part number to ensure the intents are different, and we + * thereby get an intent for each msg part. + * setType is needed to create different intents for each message id/ time stamp, + * as the extras are not used when comparing. */ + intentSent.setType("message/" + Long.toString(msgInfo.id) + msgInfo.timestamp + i); + intentSent.putExtra(EXTRA_MESSAGE_SENT_HANDLE, msgInfo.id); + intentSent.putExtra(EXTRA_MESSAGE_SENT_URI, msgInfo.uri.toString()); + intentSent.putExtra(EXTRA_MESSAGE_SENT_RETRY, msgInfo.retry); + intentSent.putExtra(EXTRA_MESSAGE_SENT_TRANSPARENT, msgInfo.transparent); + + PendingIntent pendingIntentSent = PendingIntent.getBroadcast(mContext, 0, + intentSent, PendingIntent.FLAG_UPDATE_CURRENT); + + // We use the same pending intent for all parts, but do not set the one shot flag. + deliveryIntents.add(pendingIntentDelivery); + sentIntents.add(pendingIntentSent); } Log.d(TAG, "sendMessage to " + msgInfo.phone); @@ -956,21 +1655,38 @@ public class BluetoothMapContentObserver { private static final String ACTION_MESSAGE_DELIVERY = "com.android.bluetooth.BluetoothMapContentObserver.action.MESSAGE_DELIVERY"; - private static final String ACTION_MESSAGE_SENT = + public static final String ACTION_MESSAGE_SENT = "com.android.bluetooth.BluetoothMapContentObserver.action.MESSAGE_SENT"; + public static final String EXTRA_MESSAGE_SENT_HANDLE = "HANDLE"; + public static final String EXTRA_MESSAGE_SENT_RESULT = "result"; + public static final String EXTRA_MESSAGE_SENT_URI = "uri"; + public static final String EXTRA_MESSAGE_SENT_RETRY = "retry"; + public static final String EXTRA_MESSAGE_SENT_TRANSPARENT = "transparent"; + public static final String EXTRA_MESSAGE_SENT_TIMESTAMP = "timestamp"; + private SmsBroadcastReceiver mSmsBroadcastReceiver = new SmsBroadcastReceiver(); + private boolean mInitialized = false; + private class SmsBroadcastReceiver extends BroadcastReceiver { private final String[] ID_PROJECTION = new String[] { Sms._ID }; - private final Uri UPDATE_STATUS_URI = Uri.parse("content://sms/status"); + private final Uri UPDATE_STATUS_URI = Uri.withAppendedPath(Sms.CONTENT_URI, "/status"); public void register() { Handler handler = new Handler(Looper.getMainLooper()); IntentFilter intentFilter = new IntentFilter(); intentFilter.addAction(ACTION_MESSAGE_DELIVERY); - intentFilter.addAction(ACTION_MESSAGE_SENT); + /* The reception of ACTION_MESSAGE_SENT have been moved to the MAP + * service, to be able to handle message sent events after a disconnect. */ + //intentFilter.addAction(ACTION_MESSAGE_SENT); + try{ + intentFilter.addDataType("message/*"); + } catch (MalformedMimeTypeException e) { + Log.e(TAG, "Wrong mime type!!!", e); + } + mContext.registerReceiver(this, intentFilter, null, handler); } @@ -985,7 +1701,7 @@ public class BluetoothMapContentObserver { @Override public void onReceive(Context context, Intent intent) { String action = intent.getAction(); - long handle = intent.getLongExtra("HANDLE", -1); + long handle = intent.getLongExtra(EXTRA_MESSAGE_SENT_HANDLE, -1); PushMsgInfo msgInfo = mPushMsgList.get(handle); Log.d(TAG, "onReceive: action" + action); @@ -996,12 +1712,36 @@ public class BluetoothMapContentObserver { } if (action.equals(ACTION_MESSAGE_SENT)) { + int result = intent.getIntExtra(EXTRA_MESSAGE_SENT_RESULT, Activity.RESULT_CANCELED); msgInfo.partsSent++; + if(result != Activity.RESULT_OK) { + // If just one of the parts in the message fails, we need to send the entire message again + msgInfo.failedSent = true; + } + if(D) Log.d(TAG, "onReceive: msgInfo.partsSent = " + msgInfo.partsSent + + ", msgInfo.parts = " + msgInfo.parts + " result = " + result); + if (msgInfo.partsSent == msgInfo.parts) { actionMessageSent(context, intent, msgInfo); } } else if (action.equals(ACTION_MESSAGE_DELIVERY)) { - msgInfo.partsDelivered++; + long timestamp = intent.getLongExtra(EXTRA_MESSAGE_SENT_TIMESTAMP, 0); + int status = -1; + if(msgInfo.timestamp == timestamp) { + msgInfo.partsDelivered++; + byte[] pdu = intent.getByteArrayExtra("pdu"); + String format = intent.getStringExtra("format"); + + SmsMessage message = SmsMessage.createFromPdu(pdu, format); + if (message == null) { + Log.d(TAG, "actionMessageDelivery: Can't get message from pdu"); + return; + } + status = message.getStatus(); + if(status != 0/*0 is success*/) { + msgInfo.statusDelivered = status; + } + } if (msgInfo.partsDelivered == msgInfo.parts) { actionMessageDelivery(context, intent, msgInfo); } @@ -1010,23 +1750,28 @@ public class BluetoothMapContentObserver { } } - private void actionMessageSent(Context context, Intent intent, - PushMsgInfo msgInfo) { - int result = getResultCode(); + private void actionMessageSent(Context context, Intent intent, PushMsgInfo msgInfo) { + /* As the MESSAGE_SENT intent is forwarded from the MAP service, we use the intent + * to carry the result, as getResult() will not return the correct value. + */ boolean delete = false; - if (result == Activity.RESULT_OK) { - Log.d(TAG, "actionMessageSent: result OK"); + if(D) Log.d(TAG,"actionMessageSent(): msgInfo.failedSent = " + msgInfo.failedSent); + + msgInfo.sendInProgress = false; + + if (msgInfo.failedSent == false) { + if(D) Log.d(TAG, "actionMessageSent: result OK"); if (msgInfo.transparent == 0) { if (!Sms.moveMessageToFolder(context, msgInfo.uri, Sms.MESSAGE_TYPE_SENT, 0)) { - Log.d(TAG, "Failed to move " + msgInfo.uri + " to SENT"); + Log.w(TAG, "Failed to move " + msgInfo.uri + " to SENT"); } } else { delete = true; } - Event evt = new Event("SendingSuccess", msgInfo.id, + Event evt = new Event(EVENT_TYPE_SENDING_SUCCESS, msgInfo.id, folderSms[Sms.MESSAGE_TYPE_SENT], null, mSmsType); sendEvent(evt); @@ -1034,20 +1779,22 @@ public class BluetoothMapContentObserver { if (msgInfo.retry == 1) { /* Notify failure, but keep message in outbox for resending */ msgInfo.resend = true; - Event evt = new Event("SendingFailure", msgInfo.id, + msgInfo.partsSent = 0; // Reset counter for the retry + msgInfo.failedSent = false; + Event evt = new Event(EVENT_TYPE_SENDING_FAILURE, msgInfo.id, folderSms[Sms.MESSAGE_TYPE_OUTBOX], null, mSmsType); sendEvent(evt); } else { if (msgInfo.transparent == 0) { if (!Sms.moveMessageToFolder(context, msgInfo.uri, Sms.MESSAGE_TYPE_FAILED, 0)) { - Log.d(TAG, "Failed to move " + msgInfo.uri + " to FAILED"); + Log.w(TAG, "Failed to move " + msgInfo.uri + " to FAILED"); } } else { delete = true; } - Event evt = new Event("SendingFailure", msgInfo.id, + Event evt = new Event(EVENT_TYPE_SENDING_FAILURE, msgInfo.id, folderSms[Sms.MESSAGE_TYPE_FAILED], null, mSmsType); sendEvent(evt); } @@ -1055,25 +1802,18 @@ public class BluetoothMapContentObserver { if (delete == true) { /* Delete from Observer message list to avoid delete notifications */ - mMsgListSms.remove(msgInfo.id); + synchronized(mMsgListSms) { + mMsgListSms.remove(msgInfo.id); + } /* Delete from DB */ mResolver.delete(msgInfo.uri, null, null); } } - private void actionMessageDelivery(Context context, Intent intent, - PushMsgInfo msgInfo) { + private void actionMessageDelivery(Context context, Intent intent, PushMsgInfo msgInfo) { Uri messageUri = intent.getData(); - byte[] pdu = intent.getByteArrayExtra("pdu"); - String format = intent.getStringExtra("format"); - - SmsMessage message = SmsMessage.createFromPdu(pdu, format); - if (message == null) { - Log.d(TAG, "actionMessageDelivery: Can't get message from pdu"); - return; - } - int status = message.getStatus(); + msgInfo.sendInProgress = false; Cursor cursor = mResolver.query(msgInfo.uri, ID_PROJECTION, null, null, null); @@ -1082,14 +1822,12 @@ public class BluetoothMapContentObserver { int messageId = cursor.getInt(0); Uri updateUri = ContentUris.withAppendedId(UPDATE_STATUS_URI, messageId); - boolean isStatusReport = message.isStatusReportMessage(); - Log.d(TAG, "actionMessageDelivery: uri=" + messageUri + ", status=" + status + - ", isStatusReport=" + isStatusReport); + if(D) Log.d(TAG, "actionMessageDelivery: uri=" + messageUri + ", status=" + msgInfo.statusDelivered); ContentValues contentValues = new ContentValues(2); - contentValues.put(Sms.STATUS, status); + contentValues.put(Sms.STATUS, msgInfo.statusDelivered); contentValues.put(Inbox.DATE_SENT, System.currentTimeMillis()); mResolver.update(updateUri, contentValues, null, null); } else { @@ -1099,12 +1837,12 @@ public class BluetoothMapContentObserver { cursor.close(); } - if (status == 0) { - Event evt = new Event("DeliverySuccess", msgInfo.id, + if (msgInfo.statusDelivered == 0) { + Event evt = new Event(EVENT_TYPE_DELEVERY_SUCCESS, msgInfo.id, folderSms[Sms.MESSAGE_TYPE_SENT], null, mSmsType); sendEvent(evt); } else { - Event evt = new Event("DeliveryFailure", msgInfo.id, + Event evt = new Event(EVENT_TYPE_SENDING_FAILURE, msgInfo.id, folderSms[Sms.MESSAGE_TYPE_SENT], null, mSmsType); sendEvent(evt); } @@ -1113,6 +1851,54 @@ public class BluetoothMapContentObserver { } } + static public void actionMessageSentDisconnected(Context context, Intent intent, int result) { + boolean delete = false; + //int retry = intent.getIntExtra(EXTRA_MESSAGE_SENT_RETRY, 0); + int transparent = intent.getIntExtra(EXTRA_MESSAGE_SENT_TRANSPARENT, 0); + String uriString = intent.getStringExtra(EXTRA_MESSAGE_SENT_URI); + if(uriString == null) { + // Nothing we can do about it, just bail out + return; + } + Uri uri = Uri.parse(uriString); + + if (result == Activity.RESULT_OK) { + Log.d(TAG, "actionMessageSentDisconnected: result OK"); + if (transparent == 0) { + if (!Sms.moveMessageToFolder(context, uri, + Sms.MESSAGE_TYPE_SENT, 0)) { + Log.d(TAG, "Failed to move " + uri + " to SENT"); + } + } else { + delete = true; + } + } else { + /*if (retry == 1) { + The retry feature only works while connected, else we fail the send, + * and move the message to failed, to let the user/app resend manually later. + } else */{ + if (transparent == 0) { + if (!Sms.moveMessageToFolder(context, uri, + Sms.MESSAGE_TYPE_FAILED, 0)) { + Log.d(TAG, "Failed to move " + uri + " to FAILED"); + } + } else { + delete = true; + } + } + } + + if (delete == true) { + /* Delete from DB */ + ContentResolver resolver = context.getContentResolver(); + if(resolver != null) { + resolver.delete(uri, null, null); + } else { + Log.w(TAG, "Unable to get resolver"); + } + } + } + private void registerPhoneServiceStateListener() { TelephonyManager tm = (TelephonyManager)mContext.getSystemService(Context.TELEPHONY_SERVICE); tm.listen(mPhoneListener, PhoneStateListener.LISTEN_SERVICE_STATE); @@ -1131,12 +1917,13 @@ public class BluetoothMapContentObserver { if (c != null && c.moveToFirst()) { do { - long id = c.getLong(c.getColumnIndex(BaseColumns._ID)); + long id = c.getLong(c.getColumnIndex(Sms._ID)); String msgBody = c.getString(c.getColumnIndex(Sms.BODY)); PushMsgInfo msgInfo = mPushMsgList.get(id); - if (msgInfo == null || msgInfo.resend == false) { + if (msgInfo == null || msgInfo.resend == false || msgInfo.sendInProgress == true) { continue; } + msgInfo.sendInProgress = true; sendMessage(msgInfo, msgBody); } while (c.moveToNext()); c.close(); @@ -1151,7 +1938,7 @@ public class BluetoothMapContentObserver { if (c != null && c.moveToFirst()) { do { - long id = c.getLong(c.getColumnIndex(BaseColumns._ID)); + long id = c.getLong(c.getColumnIndex(Sms._ID)); String msgBody = c.getString(c.getColumnIndex(Sms.BODY)); PushMsgInfo msgInfo = mPushMsgList.get(id); if (msgInfo == null || msgInfo.resend == false) { @@ -1166,7 +1953,7 @@ public class BluetoothMapContentObserver { private void removeDeletedMessages() { /* Remove messages from virtual "deleted" folder (thread_id -1) */ - mResolver.delete(Uri.parse("content://sms/"), + mResolver.delete(Sms.CONTENT_URI, "thread_id = " + DELETED_THREAD_ID, null); } @@ -1183,12 +1970,24 @@ public class BluetoothMapContentObserver { public void init() { mSmsBroadcastReceiver.register(); registerPhoneServiceStateListener(); + mInitialized = true; } public void deinit() { + mInitialized = false; + unregisterObserver(); mSmsBroadcastReceiver.unregister(); unRegisterPhoneServiceStateListener(); failPendingMessages(); removeDeletedMessages(); } + + public boolean handleSmsSendIntent(Context context, Intent intent){ + if(mInitialized) { + mSmsBroadcastReceiver.onReceive(context, intent); + return true; + } + return false; + } + } |