/* * 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 * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.bluetooth.map; import android.annotation.TargetApi; 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.content.pm.PackageManager; import android.database.ContentObserver; import android.database.Cursor; import android.database.sqlite.SQLiteException; import android.net.Uri; import android.os.Binder; import android.os.Handler; import android.os.Looper; import android.os.Message; import android.os.ParcelFileDescriptor; import android.os.Process; import android.os.RemoteException; import android.os.UserManager; import android.provider.Telephony; import android.provider.Telephony.Mms; import android.provider.Telephony.MmsSms; import android.provider.Telephony.Sms; import android.provider.Telephony.Sms.Inbox; import android.telephony.PhoneStateListener; import android.telephony.ServiceState; import android.telephony.SmsManager; import android.telephony.TelephonyManager; import android.text.format.DateUtils; import android.text.TextUtils; import android.util.Log; import android.util.Xml; import org.xmlpull.v1.XmlSerializer; import com.android.bluetooth.map.BluetoothMapUtils.TYPE; import com.android.bluetooth.map.BluetoothMapbMessageMime.MimePart; import com.android.bluetooth.mapapi.BluetoothMapContract; import com.android.bluetooth.mapapi.BluetoothMapContract.MessageColumns; import com.google.android.mms.pdu.PduHeaders; 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.Objects; import java.util.Set; import javax.obex.ResponseCodes; @TargetApi(19) public class BluetoothMapContentObserver { private static final String TAG = "BluetoothMapContentObserver"; private static final boolean D = BluetoothMapService.DEBUG; private static final boolean V = BluetoothMapService.VERBOSE; private static final String EVENT_TYPE_NEW = "NewMessage"; private static final String EVENT_TYPE_DELETE = "MessageDeleted"; private static final String EVENT_TYPE_REMOVED = "MessageRemoved"; private static final String EVENT_TYPE_SHIFT = "MessageShift"; 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 String EVENT_TYPE_READ_STATUS = "ReadStatusChanged"; private static final String EVENT_TYPE_CONVERSATION = "ConversationChanged"; private static final String EVENT_TYPE_PRESENCE = "ParticipantPresenceChanged"; private static final String EVENT_TYPE_CHAT_STATE = "ParticipantChatStateChanged"; private static final long EVENT_FILTER_NEW_MESSAGE = 1L; private static final long EVENT_FILTER_MESSAGE_DELETED = 1L<<1; private static final long EVENT_FILTER_MESSAGE_SHIFT = 1L<<2; private static final long EVENT_FILTER_SENDING_SUCCESS = 1L<<3; private static final long EVENT_FILTER_SENDING_FAILED = 1L<<4; private static final long EVENT_FILTER_DELIVERY_SUCCESS = 1L<<5; private static final long EVENT_FILTER_DELIVERY_FAILED = 1L<<6; private static final long EVENT_FILTER_MEMORY_FULL = 1L<<7; // Unused private static final long EVENT_FILTER_MEMORY_AVAILABLE = 1L<<8; // Unused private static final long EVENT_FILTER_READ_STATUS_CHANGED = 1L<<9; private static final long EVENT_FILTER_CONVERSATION_CHANGED = 1L<<10; private static final long EVENT_FILTER_PARTICIPANT_PRESENCE_CHANGED = 1L<<11; private static final long EVENT_FILTER_PARTICIPANT_CHATSTATE_CHANGED= 1L<<12; private static final long EVENT_FILTER_MESSAGE_REMOVED = 1L<<13; // TODO: If we are requesting a large message from the network, on a slow connection // 20 seconds might not be enough... But then again 20 seconds is long for other // cases. 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 BluetoothMapAccountItem mAccount; private String mAuthority = null; // Default supported feature bit mask is 0x1f private int mMapSupportedFeatures = BluetoothMapUtils.MAP_FEATURE_DEFAULT_BITMASK; // Default event report version is 1.0 private int mMapEventReportVersion = BluetoothMapUtils.MAP_EVENT_REPORT_V10; private BluetoothMapFolderElement mFolders = new BluetoothMapFolderElement("DUMMY", null); // Will be set by the MAS when generated. private Uri mMessageUri = null; private Uri mContactUri = null; private boolean mTransmitEvents = true; /* To make the filter update atomic, we declare it volatile. * To avoid a penalty when using it, copy the value to a local * non-volatile variable when used more than once. * Actually we only ever use the lower 4 bytes of this variable, * hence we could manage without the volatile keyword, but as * we tend to copy ways of doing things, we better do it right:-) */ private volatile long mEventFilter = 0xFFFFFFFFL; public static final int DELETED_THREAD_ID = -1; // 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; private static final String ACTION_MESSAGE_DELIVERY = "com.android.bluetooth.BluetoothMapContentObserver.action.MESSAGE_DELIVERY"; /*package*/ 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_MSG_TYPE = "type"; 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 CeBroadcastReceiver mCeBroadcastReceiver = new CeBroadcastReceiver(); private boolean mStorageUnlocked = false; private boolean mInitialized = false; static final String[] SMS_PROJECTION = new String[] { Sms._ID, Sms.THREAD_ID, Sms.ADDRESS, Sms.BODY, Sms.DATE, Sms.READ, Sms.TYPE, Sms.STATUS, Sms.LOCKED, Sms.ERROR_CODE }; static final String[] SMS_PROJECTION_SHORT = new String[] { Sms._ID, Sms.THREAD_ID, Sms.TYPE, Sms.READ }; static final String[] SMS_PROJECTION_SHORT_EXT = new String[] { Sms._ID, Sms.THREAD_ID, Sms.ADDRESS, Sms.BODY, Sms.DATE, Sms.READ, Sms.TYPE, }; static final String[] MMS_PROJECTION_SHORT = new String[] { Mms._ID, Mms.THREAD_ID, Mms.MESSAGE_TYPE, Mms.MESSAGE_BOX, Mms.READ }; static final String[] MMS_PROJECTION_SHORT_EXT = new String[] { Mms._ID, Mms.THREAD_ID, Mms.MESSAGE_TYPE, Mms.MESSAGE_BOX, Mms.READ, Mms.DATE, Mms.SUBJECT, Mms.PRIORITY }; static final String[] MSG_PROJECTION_SHORT = new String[] { BluetoothMapContract.MessageColumns._ID, BluetoothMapContract.MessageColumns.FOLDER_ID, BluetoothMapContract.MessageColumns.FLAG_READ }; static final String[] MSG_PROJECTION_SHORT_EXT = new String[] { BluetoothMapContract.MessageColumns._ID, BluetoothMapContract.MessageColumns.FOLDER_ID, BluetoothMapContract.MessageColumns.FLAG_READ, BluetoothMapContract.MessageColumns.DATE, BluetoothMapContract.MessageColumns.SUBJECT, BluetoothMapContract.MessageColumns.FROM_LIST, BluetoothMapContract.MessageColumns.FLAG_HIGH_PRIORITY }; static final String[] MSG_PROJECTION_SHORT_EXT2 = new String[] { BluetoothMapContract.MessageColumns._ID, BluetoothMapContract.MessageColumns.FOLDER_ID, BluetoothMapContract.MessageColumns.FLAG_READ, BluetoothMapContract.MessageColumns.DATE, BluetoothMapContract.MessageColumns.SUBJECT, BluetoothMapContract.MessageColumns.FROM_LIST, BluetoothMapContract.MessageColumns.FLAG_HIGH_PRIORITY, BluetoothMapContract.MessageColumns.THREAD_ID, BluetoothMapContract.MessageColumns.THREAD_NAME }; public BluetoothMapContentObserver(final Context context, BluetoothMnsObexClient mnsClient, BluetoothMapMasInstance masInstance, BluetoothMapAccountItem account, boolean enableSmsMms) throws RemoteException { mContext = context; mResolver = mContext.getContentResolver(); mAccount = account; mMasInstance = masInstance; mMasId = mMasInstance.getMasId(); setObserverRemoteFeatureMask(mMasInstance.getRemoteFeatureMask()); if(account != null) { mAuthority = Uri.parse(account.mBase_uri).getAuthority(); mMessageUri = Uri.parse(account.mBase_uri + "/" + BluetoothMapContract.TABLE_MESSAGE); if (mAccount.getType() == TYPE.IM) { mContactUri = Uri.parse(account.mBase_uri + "/" + BluetoothMapContract.TABLE_CONVOCONTACT); } // TODO: We need to release this again! mProviderClient = mResolver.acquireUnstableContentProviderClient(mAuthority); if (mProviderClient == null) { throw new RemoteException("Failed to acquire provider for " + mAuthority); } mProviderClient.setDetectNotResponding(PROVIDER_ANR_TIMEOUT); mContactList = mMasInstance.getContactList(); if(mContactList == null) { setContactList(new HashMap(), false); initContactsList(); } } mEnableSmsMms = enableSmsMms; mSmsType = getSmsType(); mMnsClient = mnsClient; /* Get the cached list - if any, else create */ mMsgListSms = mMasInstance.getMsgListSms(); boolean doInit = false; if(mEnableSmsMms) { if(mMsgListSms == null) { setMsgListSms(new HashMap(), false); doInit = true; } mMsgListMms = mMasInstance.getMsgListMms(); if(mMsgListMms == null) { setMsgListMms(new HashMap(), false); doInit = true; } } if(mAccount != null) { mMsgListMsg = mMasInstance.getMsgListMsg(); if(mMsgListMsg == null) { setMsgListMsg(new HashMap(), false); doInit = true; } } if(doInit) { initMsgList(); } } public int getObserverRemoteFeatureMask() { if (V) Log.v(TAG, "getObserverRemoteFeatureMask : " + mMapEventReportVersion + " mMapSupportedFeatures: " + mMapSupportedFeatures); return mMapSupportedFeatures; } public void setObserverRemoteFeatureMask(int remoteSupportedFeatures) { mMapSupportedFeatures = remoteSupportedFeatures & BluetoothMapMasInstance.SDP_MAP_MAS_FEATURES; if ((BluetoothMapUtils.MAP_FEATURE_EXTENDED_EVENT_REPORT_11_BIT & mMapSupportedFeatures) != 0) { mMapEventReportVersion = BluetoothMapUtils.MAP_EVENT_REPORT_V11; } // Make sure support for all formats result in latest version returned if ((BluetoothMapUtils.MAP_FEATURE_EVENT_REPORT_V12_BIT & mMapSupportedFeatures) != 0) { mMapEventReportVersion = BluetoothMapUtils.MAP_EVENT_REPORT_V12; } else if (((BluetoothMapUtils.MAP_FEATURE_PARTICIPANT_PRESENCE_CHANGE_BIT | BluetoothMapUtils.MAP_FEATURE_PARTICIPANT_CHAT_STATE_CHANGE_BIT) & mMapSupportedFeatures) != 0) { // Warning according to page 46/123 of MAP 1.3 spec Log.w(TAG, "setObserverRemoteFeatureMask: Extended Event Reports 1.2 is not set even" + "though PARTICIPANT_PRESENCE_CHANGE_BIT or PARTICIPANT_CHAT_STATE_CHANGE_BIT" + " were set, mMapSupportedFeatures=" + mMapSupportedFeatures); } if (D) Log.d(TAG, "setObserverRemoteFeatureMask: mMapEventReportVersion=" + mMapEventReportVersion + " mMapSupportedFeatures=" + mMapSupportedFeatures); } private Map getMsgListSms() { return mMsgListSms; } private void setMsgListSms(Map msgListSms, boolean changesDetected) { mMsgListSms = msgListSms; if(changesDetected) { mMasInstance.updateFolderVersionCounter(); } mMasInstance.setMsgListSms(msgListSms); } private Map getMsgListMms() { return mMsgListMms; } private void setMsgListMms(Map msgListMms, boolean changesDetected) { mMsgListMms = msgListMms; if(changesDetected) { mMasInstance.updateFolderVersionCounter(); } mMasInstance.setMsgListMms(msgListMms); } private Map getMsgListMsg() { return mMsgListMsg; } private void setMsgListMsg(Map msgListMsg, boolean changesDetected) { mMsgListMsg = msgListMsg; if(changesDetected) { mMasInstance.updateFolderVersionCounter(); } mMasInstance.setMsgListMsg(msgListMsg); } private Map getContactList() { return mContactList; } /** * Currently we only have data for IM / email contacts * @param contactList * @param changesDetected that is not chat state changed nor presence state changed. */ private void setContactList(Map contactList, boolean changesDetected) { mContactList = contactList; if(changesDetected) { mMasInstance.updateImEmailConvoListVersionCounter(); } mMasInstance.setContactList(contactList); } private static boolean sendEventNewMessage(long eventFilter) { return ((eventFilter & EVENT_FILTER_NEW_MESSAGE) > 0); } private static boolean sendEventMessageDeleted(long eventFilter) { return ((eventFilter & EVENT_FILTER_MESSAGE_DELETED) > 0); } private static boolean sendEventMessageShift(long eventFilter) { return ((eventFilter & EVENT_FILTER_MESSAGE_SHIFT) > 0); } private static boolean sendEventSendingSuccess(long eventFilter) { return ((eventFilter & EVENT_FILTER_SENDING_SUCCESS) > 0); } private static boolean sendEventSendingFailed(long eventFilter) { return ((eventFilter & EVENT_FILTER_SENDING_FAILED) > 0); } private static boolean sendEventDeliverySuccess(long eventFilter) { return ((eventFilter & EVENT_FILTER_DELIVERY_SUCCESS) > 0); } private static boolean sendEventDeliveryFailed(long eventFilter) { return ((eventFilter & EVENT_FILTER_DELIVERY_FAILED) > 0); } private static boolean sendEventReadStatusChanged(long eventFilter) { return ((eventFilter & EVENT_FILTER_READ_STATUS_CHANGED) > 0); } private static boolean sendEventConversationChanged(long eventFilter) { return ((eventFilter & EVENT_FILTER_CONVERSATION_CHANGED) > 0); } private static boolean sendEventParticipantPresenceChanged(long eventFilter) { return ((eventFilter & EVENT_FILTER_PARTICIPANT_PRESENCE_CHANGED) > 0); } private static boolean sendEventParticipantChatstateChanged(long eventFilter) { return ((eventFilter & EVENT_FILTER_PARTICIPANT_CHATSTATE_CHANGED) > 0); } private static boolean sendEventMessageRemoved(long eventFilter) { return ((eventFilter & EVENT_FILTER_MESSAGE_REMOVED) > 0); } private TYPE getSmsType() { TYPE smsType = null; TelephonyManager tm = (TelephonyManager) mContext.getSystemService( Context.TELEPHONY_SERVICE); if (tm.getPhoneType() == TelephonyManager.PHONE_TYPE_CDMA) { smsType = TYPE.SMS_CDMA; } else { smsType = TYPE.SMS_GSM; } return smsType; } private final ContentObserver mObserver = new ContentObserver(new Handler()) { @Override public void onChange(boolean selfChange) { onChange(selfChange, null); } @Override public void onChange(boolean selfChange, Uri uri) { if(uri == null) { Log.w(TAG, "onChange() with URI == null - not handled."); return; } if (!mStorageUnlocked) { Log.v(TAG, "Ignore events until storage is completely unlocked"); return; } if (V) Log.d(TAG, "onChange on thread: " + Thread.currentThread().getId() + " Uri: " + uri.toString() + " selfchange: " + selfChange); if(uri.toString().contains(BluetoothMapContract.TABLE_CONVOCONTACT)) handleContactListChanges(uri); else handleMsgListChanges(uri); } }; private static final HashMap FOLDER_SMS_MAP; static { FOLDER_SMS_MAP = new HashMap(); FOLDER_SMS_MAP.put(Sms.MESSAGE_TYPE_INBOX, BluetoothMapContract.FOLDER_NAME_INBOX); FOLDER_SMS_MAP.put(Sms.MESSAGE_TYPE_SENT, BluetoothMapContract.FOLDER_NAME_SENT); FOLDER_SMS_MAP.put(Sms.MESSAGE_TYPE_DRAFT, BluetoothMapContract.FOLDER_NAME_DRAFT); FOLDER_SMS_MAP.put(Sms.MESSAGE_TYPE_OUTBOX, BluetoothMapContract.FOLDER_NAME_OUTBOX); FOLDER_SMS_MAP.put(Sms.MESSAGE_TYPE_FAILED, BluetoothMapContract.FOLDER_NAME_OUTBOX); FOLDER_SMS_MAP.put(Sms.MESSAGE_TYPE_QUEUED, BluetoothMapContract.FOLDER_NAME_OUTBOX); } private static String getSmsFolderName(int type) { String name = FOLDER_SMS_MAP.get(type); if(name != null) { return name; } Log.e(TAG, "New SMS mailbox types have been introduced, without an update in BT..."); return "Unknown"; } private static final HashMap FOLDER_MMS_MAP; static { FOLDER_MMS_MAP = new HashMap(); FOLDER_MMS_MAP.put(Mms.MESSAGE_BOX_INBOX, BluetoothMapContract.FOLDER_NAME_INBOX); FOLDER_MMS_MAP.put(Mms.MESSAGE_BOX_SENT, BluetoothMapContract.FOLDER_NAME_SENT); FOLDER_MMS_MAP.put(Mms.MESSAGE_BOX_DRAFTS, BluetoothMapContract.FOLDER_NAME_DRAFT); FOLDER_MMS_MAP.put(Mms.MESSAGE_BOX_OUTBOX, BluetoothMapContract.FOLDER_NAME_OUTBOX); } private static String getMmsFolderName(int mailbox) { String name = FOLDER_MMS_MAP.get(mailbox); if(name != null) { return name; } Log.e(TAG, "New MMS mailboxes have been introduced, without an update in BT..."); return "Unknown"; } /** * Set the folder structure to be used for this instance. * @param folderStructure */ public void setFolderStructure(BluetoothMapFolderElement folderStructure) { this.mFolders = folderStructure; } private class ConvoContactInfo { public int mConvoColConvoId = -1; public int mConvoColLastActivity = -1; public int mConvoColName = -1; // public int mConvoColRead = -1; // public int mConvoColVersionCounter = -1; public int mContactColUci = -1; public int mContactColConvoId = -1; public int mContactColName = -1; public int mContactColNickname = -1; public int mContactColBtUid = -1; public int mContactColChatState = -1; public int mContactColContactId = -1; public int mContactColLastActive = -1; public int mContactColPresenceState = -1; public int mContactColPresenceText = -1; public int mContactColPriority = -1; public int mContactColLastOnline = -1; public void setConvoColunms(Cursor c) { // mConvoColConvoId = c.getColumnIndex( // BluetoothMapContract.ConversationColumns.THREAD_ID); // mConvoColLastActivity = c.getColumnIndex( // BluetoothMapContract.ConversationColumns.LAST_THREAD_ACTIVITY); // mConvoColName = c.getColumnIndex( // BluetoothMapContract.ConversationColumns.THREAD_NAME); mContactColConvoId = c.getColumnIndex( BluetoothMapContract.ConvoContactColumns.CONVO_ID); mContactColName = c.getColumnIndex( BluetoothMapContract.ConvoContactColumns.NAME); mContactColNickname = c.getColumnIndex( BluetoothMapContract.ConvoContactColumns.NICKNAME); mContactColBtUid = c.getColumnIndex( BluetoothMapContract.ConvoContactColumns.X_BT_UID); mContactColChatState = c.getColumnIndex( BluetoothMapContract.ConvoContactColumns.CHAT_STATE); mContactColUci = c.getColumnIndex( BluetoothMapContract.ConvoContactColumns.UCI); mContactColNickname = c.getColumnIndex( BluetoothMapContract.ConvoContactColumns.NICKNAME); mContactColLastActive = c.getColumnIndex( BluetoothMapContract.ConvoContactColumns.LAST_ACTIVE); mContactColName = c.getColumnIndex( BluetoothMapContract.ConvoContactColumns.NAME); mContactColPresenceState = c.getColumnIndex( BluetoothMapContract.ConvoContactColumns.PRESENCE_STATE); mContactColPresenceText = c.getColumnIndex( BluetoothMapContract.ConvoContactColumns.STATUS_TEXT); mContactColPriority = c.getColumnIndex( BluetoothMapContract.ConvoContactColumns.PRIORITY); mContactColLastOnline = c.getColumnIndex( BluetoothMapContract.ConvoContactColumns.LAST_ONLINE); } } private class Event { String eventType; long handle; String folder = null; String oldFolder = null; TYPE msgType; /* Extended event parameters in MAP Event version 1.1 */ String datetime = null; // OBEX time "YYYYMMDDTHHMMSS" String uci = null; String subject = null; String senderName = null; String priority = null; /* Event parameters in MAP Event version 1.2 */ String conversationName = null; long conversationID = -1; int presenceState = BluetoothMapContract.PresenceState.UNKNOWN; String presenceStatus = null; int chatState = BluetoothMapContract.ChatState.UNKNOWN; static final String PATH = "telecom/msg/"; private void setFolderPath(String name, TYPE type) { if (name != null) { if(type == TYPE.EMAIL || type == TYPE.IM) { this.folder = name; } else { this.folder = PATH + name; } } else { this.folder = null; } } Event(String eventType, long handle, String folder, String oldFolder, TYPE msgType) { this.eventType = eventType; this.handle = handle; setFolderPath(folder, msgType); if (oldFolder != null) { if(msgType == TYPE.EMAIL || msgType == TYPE.IM) { this.oldFolder = oldFolder; } else { this.oldFolder = PATH + oldFolder; } } else { this.oldFolder = null; } this.msgType = msgType; } Event(String eventType, long handle, String folder, TYPE msgType) { this.eventType = eventType; this.handle = handle; setFolderPath(folder, msgType); this.msgType = msgType; } /* extended event type 1.1 */ Event(String eventType, long handle, String folder, TYPE msgType, String datetime, String subject, String senderName, String priority) { this.eventType = eventType; this.handle = handle; setFolderPath(folder, msgType); this.msgType = msgType; this.datetime = datetime; if (subject != null) { this.subject = BluetoothMapUtils.stripInvalidChars(subject); } if (senderName != null) { this.senderName = BluetoothMapUtils.stripInvalidChars(senderName); } this.priority = priority; } /* extended event type 1.2 message events */ Event(String eventType, long handle, String folder, TYPE msgType, String datetime, String subject, String senderName, String priority, long conversationID, String conversationName) { this.eventType = eventType; this.handle = handle; setFolderPath(folder, msgType); this.msgType = msgType; this.datetime = datetime; if (subject != null) { this.subject = BluetoothMapUtils.stripInvalidChars(subject); } if (senderName != null) { this.senderName = BluetoothMapUtils.stripInvalidChars(senderName); } if (conversationID != 0) { this.conversationID = conversationID; } if (conversationName != null) { this.conversationName = BluetoothMapUtils.stripInvalidChars(conversationName); } this.priority = priority; } /* extended event type 1.2 for conversation, presence or chat state changed events */ Event(String eventType, String uci, TYPE msgType, String name, String priority, String lastActivity, long conversationID, String conversationName, int presenceState, String presenceStatus, int chatState) { this.eventType = eventType; this.uci = uci; this.msgType = msgType; if (name != null) { this.senderName = BluetoothMapUtils.stripInvalidChars(name); } this.priority = priority; this.datetime = lastActivity; if (conversationID != 0) { this.conversationID = conversationID; } if (conversationName != null) { this.conversationName = BluetoothMapUtils.stripInvalidChars(conversationName); } if (presenceState != BluetoothMapContract.PresenceState.UNKNOWN) { this.presenceState = presenceState; } if (presenceStatus != null) { this.presenceStatus = BluetoothMapUtils.stripInvalidChars(presenceStatus); } if (chatState != BluetoothMapContract.ChatState.UNKNOWN) { this.chatState = chatState; } } public byte[] encode() throws UnsupportedEncodingException { StringWriter sw = new StringWriter(); XmlSerializer xmlEvtReport = Xml.newSerializer(); try { xmlEvtReport.setOutput(sw); xmlEvtReport.startDocument("UTF-8", true); xmlEvtReport.text("\r\n"); xmlEvtReport.startTag("", "MAP-event-report"); if (mMapEventReportVersion == BluetoothMapUtils.MAP_EVENT_REPORT_V12) { xmlEvtReport.attribute("", "version", BluetoothMapUtils.MAP_V12_STR); } else if (mMapEventReportVersion == BluetoothMapUtils.MAP_EVENT_REPORT_V11) { xmlEvtReport.attribute("", "version", BluetoothMapUtils.MAP_V11_STR); } else { xmlEvtReport.attribute("", "version", BluetoothMapUtils.MAP_V10_STR); } xmlEvtReport.startTag("", "event"); xmlEvtReport.attribute("", "type", eventType); if (eventType.equals(EVENT_TYPE_CONVERSATION) || eventType.equals(EVENT_TYPE_PRESENCE) || eventType.equals(EVENT_TYPE_CHAT_STATE)) { xmlEvtReport.attribute("", "participant_uci", uci); } else { xmlEvtReport.attribute("", "handle", BluetoothMapUtils.getMapHandle(handle, msgType)); } if (folder != null) { xmlEvtReport.attribute("", "folder", folder); } if (oldFolder != null) { xmlEvtReport.attribute("", "old_folder", oldFolder); } /* Avoid possible NPE for "msgType" "null" value. "msgType" * is a implied attribute and will be set "null" for events * like "memory full" or "memory available" */ if (msgType != null) { xmlEvtReport.attribute("", "msg_type", msgType.name()); } /* If MAP event report version is above 1.0 send * extended event report parameters */ if (datetime != null) { xmlEvtReport.attribute("", "datetime", datetime); } if (subject != null) { xmlEvtReport.attribute("", "subject", subject.substring(0,subject.length() < 256 ? subject.length() : 256)); } if (senderName != null) { xmlEvtReport.attribute("", "sender_name", senderName); } if (priority != null) { xmlEvtReport.attribute("", "priority", priority); } //} /* Include conversation information from event version 1.2 */ if (mMapEventReportVersion > BluetoothMapUtils.MAP_EVENT_REPORT_V11 ) { if (conversationName != null) { xmlEvtReport.attribute("", "conversation_name", conversationName); } if (conversationID != -1) { // Convert provider conversation handle to string incl type xmlEvtReport.attribute("", "conversation_id", BluetoothMapUtils.getMapConvoHandle(conversationID, msgType)); } if (eventType.equals(EVENT_TYPE_PRESENCE)) { if (presenceState != 0) { // Convert provider conversation handle to string incl type xmlEvtReport.attribute("", "presence_availability", String.valueOf(presenceState)); } if (presenceStatus != null) { // Convert provider conversation handle to string incl type xmlEvtReport.attribute("", "presence_status", presenceStatus.substring( 0,presenceStatus.length() < 256 ? subject.length() : 256)); } } if (eventType.equals(EVENT_TYPE_PRESENCE)) { if (chatState != 0) { // Convert provider conversation handle to string incl type xmlEvtReport.attribute("", "chat_state", String.valueOf(chatState)); } } } xmlEvtReport.endTag("", "event"); xmlEvtReport.endTag("", "MAP-event-report"); xmlEvtReport.endDocument(); } catch (IllegalArgumentException e) { if(D) Log.w(TAG,e); } catch (IllegalStateException e) { if(D) Log.w(TAG,e); } catch (IOException e) { if(D) Log.w(TAG,e); } if (V) Log.d(TAG, sw.toString()); return sw.toString().getBytes("UTF-8"); } } /*package*/ class Msg { long id; 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 int flagRead = -1; // Message status read/unread Msg(long id, int type, int threadId, int readFlag) { this.id = id; this.type = type; this.threadId = threadId; this.flagRead = readFlag; } Msg(long id, long folderId, int readFlag) { this.id = id; this.folderId = folderId; this.flagRead = readFlag; } /* 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; } @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 mMsgListSms = null; private Map mMsgListMms = null; private Map mMsgListMsg = null; private Map mContactList = null; 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"); if (mMnsClient == null) { return ResponseCodes.OBEX_HTTP_UNAVAILABLE; } Handler mns = mMnsClient.getMessageHandler(); if (mns != null) { Message msg = mns.obtainMessage(); if (mMnsClient.isValidMnsRecord()) { msg.what = BluetoothMnsObexClient.MSG_MNS_NOTIFICATION_REGISTRATION; } else { //Trigger SDP Search and notificaiton registration , if SDP record not found. msg.what = BluetoothMnsObexClient.MSG_MNS_SDP_SEARCH_REGISTRATION; if (mMnsClient.mMnsLstRegRqst != null && (mMnsClient.mMnsLstRegRqst.isSearchInProgress())) { /* 1. Disallow next Notification ON Request : * - Respond "Service Unavailable" as SDP Search and last notification * registration ON request is already InProgress. * - Next notification ON Request will be allowed ONLY after search * and connect for last saved request [Replied with OK ] is processed. */ if (notificationStatus == BluetoothMapAppParams.NOTIFICATION_STATUS_YES) { return ResponseCodes.OBEX_HTTP_UNAVAILABLE; } else { /* 2. Allow next Notification OFF Request: * - Keep the SDP search still in progress. * - Disconnect and Deregister the contentObserver. */ 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() send : " + msg.what + " to MNS "); return ResponseCodes.OBEX_HTTP_OK; } else { // This should not happen except at shutdown. if(D) Log.d(TAG,"setNotificationRegistration() Unable to send registration request"); return ResponseCodes.OBEX_HTTP_UNAVAILABLE; } } boolean eventMaskContainsContacts(long mask) { return sendEventParticipantPresenceChanged(mask); } boolean eventMaskContainsCovo(long mask) { return (sendEventConversationChanged(mask) || sendEventParticipantChatstateChanged(mask)); } /* Overwrite the existing notification filter. Will register/deregister observers for * the Contacts and Conversation table as needed. We keep the message observer * at all times. */ /*package*/ synchronized void setNotificationFilter(long newFilter) { long oldFilter = mEventFilter; mEventFilter = newFilter; /* Contacts */ if(!eventMaskContainsContacts(oldFilter) && eventMaskContainsContacts(newFilter)) { // TODO: // Enable the observer // Reset the contacts list } /* Conversations */ if(!eventMaskContainsCovo(oldFilter) && eventMaskContainsCovo(newFilter)) { // TODO: // Enable the observer // Reset the conversations list } } public void registerObserver() throws RemoteException{ if (V) Log.d(TAG, "registerObserver"); if (mObserverRegistered) return; if(mAccount != null) { mProviderClient = mResolver.acquireUnstableContentProviderClient(mAuthority); if (mProviderClient == null) { throw new RemoteException("Failed to acquire provider for " + mAuthority); } mProviderClient.setDetectNotResponding(PROVIDER_ANR_TIMEOUT); // If there is a change in the database before we init the lists we will be sending // loads of events - hence init before register. if(mAccount.getType() == TYPE.IM) { // Further add contact list tracking initContactsList(); } } // If there is a change in the database before we init the lists we will be sending // loads of events - hence init before register. initMsgList(); /* Use MmsSms Uri since the Sms Uri is not notified on deletes */ if(mEnableSmsMms){ //this is sms/mms mResolver.registerContentObserver(MmsSms.CONTENT_URI, false, mObserver); mObserverRegistered = true; } if(mAccount != null) { /* 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); if(mAccount.getType() == TYPE.IM) { uri = Uri.parse(mAccount.mBase_uri_no_account + "/" + BluetoothMapContract.TABLE_CONVOCONTACT); 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_CONVOCONTACT); if(D) Log.d(TAG, "Registering observer for: " + uri); mResolver.registerContentObserver(uri, true, mObserver); } mObserverRegistered = true; } } public void unregisterObserver() { if (V) Log.d(TAG, "unregisterObserver"); mResolver.unregisterContentObserver(mObserver); mObserverRegistered = false; if(mProviderClient != null){ mProviderClient.release(); mProviderClient = null; } } /** * Per design it is only possible to call the refreshXxxx functions sequentially, hence it * is safe to modify mTransmitEvents without synchronization. */ /* package */ void refreshFolderVersionCounter() { if (mObserverRegistered) { // As we have observers, we already keep the counter up-to-date. return; } /* We need to perform the same functionality, as when we receive a notification change, hence we: - disable the event transmission - triggers the code for updates - enable the event transmission */ mTransmitEvents = false; try { if(mEnableSmsMms) { handleMsgListChangesSms(); handleMsgListChangesMms(); } if(mAccount != null) { try { handleMsgListChangesMsg(mMessageUri); } catch (RemoteException e) { Log.e(TAG, "Unable to update FolderVersionCounter. - Not fatal, but can cause" + " undesirable user experience!", e); } } } finally { // Ensure we always enable events again mTransmitEvents = true; } } /* package */ void refreshConvoListVersionCounter() { if (mObserverRegistered) { // As we have observers, we already keep the counter up-to-date. return; } /* We need to perform the same functionality, as when we receive a notification change, hence we: - disable event transmission - triggers the code for updates - enable event transmission */ mTransmitEvents = false; try { if((mAccount != null) && (mContactUri != null)) { handleContactListChanges(mContactUri); } } finally { // Ensure we always enable events again mTransmitEvents = true; } } private void sendEvent(Event evt) { if(!mTransmitEvents) { if(V) Log.v(TAG, "mTransmitEvents == false - don't send event."); return; } if(D)Log.d(TAG, "sendEvent: " + evt.eventType + " " + evt.handle + " " + evt.folder + " " + evt.oldFolder + " " + evt.msgType.name() + " " + evt.datetime + " " + evt.subject + " " + evt.senderName + " " + evt.priority ); if (mMnsClient == null || !mMnsClient.isConnected()) { Log.d(TAG, "sendEvent: No MNS client registered or connected- don't send event"); return; } /* Enable use of the cache for checking the filter */ long eventFilter = mEventFilter; /* This should have been a switch on the string, but it is not allowed in Java 1.6 */ /* WARNING: Here we do pointer compare for the string to speed up things, that is. * HENCE: always use the EVENT_TYPE_"defines" */ if(Objects.equals(evt.eventType, EVENT_TYPE_NEW)) { if(!sendEventNewMessage(eventFilter)) { if(D)Log.d(TAG, "Skip sending event of type: " + evt.eventType); return; } } else if(Objects.equals(evt.eventType, EVENT_TYPE_DELETE)) { if(!sendEventMessageDeleted(eventFilter)) { if(D)Log.d(TAG, "Skip sending event of type: " + evt.eventType); return; } } else if(Objects.equals(evt.eventType, EVENT_TYPE_REMOVED)) { if(!sendEventMessageRemoved(eventFilter)) { if(D)Log.d(TAG, "Skip sending event of type: " + evt.eventType); return; } } else if(Objects.equals(evt.eventType, EVENT_TYPE_SHIFT)) { if(!sendEventMessageShift(eventFilter)) { if(D)Log.d(TAG, "Skip sending event of type: " + evt.eventType); return; } } else if(Objects.equals(evt.eventType, EVENT_TYPE_DELEVERY_SUCCESS)) { if(!sendEventDeliverySuccess(eventFilter)) { if(D)Log.d(TAG, "Skip sending event of type: " + evt.eventType); return; } } else if(Objects.equals(evt.eventType, EVENT_TYPE_SENDING_SUCCESS)) { if(!sendEventSendingSuccess(eventFilter)) { if(D)Log.d(TAG, "Skip sending event of type: " + evt.eventType); return; } } else if(Objects.equals(evt.eventType, EVENT_TYPE_SENDING_FAILURE)) { if(!sendEventSendingFailed(eventFilter)) { if(D)Log.d(TAG, "Skip sending event of type: " + evt.eventType); return; } } else if(Objects.equals(evt.eventType, EVENT_TYPE_DELIVERY_FAILURE)) { if(!sendEventDeliveryFailed(eventFilter)) { if(D)Log.d(TAG, "Skip sending event of type: " + evt.eventType); return; } } else if(Objects.equals(evt.eventType, EVENT_TYPE_READ_STATUS)) { if(!sendEventReadStatusChanged(eventFilter)) { if(D)Log.d(TAG, "Skip sending event of type: " + evt.eventType); return; } } else if(Objects.equals(evt.eventType, EVENT_TYPE_CONVERSATION)) { if(!sendEventConversationChanged(eventFilter)) { if(D)Log.d(TAG, "Skip sending event of type: " + evt.eventType); return; } } else if(Objects.equals(evt.eventType, EVENT_TYPE_PRESENCE)) { if(!sendEventParticipantPresenceChanged(eventFilter)) { if(D)Log.d(TAG, "Skip sending event of type: " + evt.eventType); return; } } else if(Objects.equals(evt.eventType, EVENT_TYPE_CHAT_STATE)) { if(!sendEventParticipantChatstateChanged(eventFilter)) { if(D)Log.d(TAG, "Skip sending event of type: " + evt.eventType); return; } } try { mMnsClient.sendEvent(evt.encode(), mMasId); } catch (UnsupportedEncodingException ex) { /* do nothing */ if (D) Log.e(TAG, "Exception - should not happen: ",ex); } } private void initMsgList() throws RemoteException { if (V) Log.d(TAG, "initMsgList"); UserManager manager = UserManager.get(mContext); if (manager == null || !manager.isUserUnlocked()) return; if (mEnableSmsMms) { HashMap msgListSms = new HashMap(); Cursor c; try { c = mResolver.query(Sms.CONTENT_URI, SMS_PROJECTION_SHORT, null, null, null); } catch (SQLiteException e) { Log.e(TAG, "Failed to initialize the list of messages: " + e.toString()); return; } try { 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)); int read = c.getInt(c.getColumnIndex(Sms.READ)); Msg msg = new Msg(id, type, threadId, read); msgListSms.put(id, msg); } while (c.moveToNext()); } } finally { if (c != null) c.close(); } synchronized(getMsgListSms()) { getMsgListSms().clear(); setMsgListSms(msgListSms, true); // Set initial folder version counter } HashMap msgListMms = new HashMap(); c = mResolver.query(Mms.CONTENT_URI, MMS_PROJECTION_SHORT, null, null, null); try { 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)); int read = c.getInt(c.getColumnIndex(Mms.READ)); Msg msg = new Msg(id, type, threadId, read); msgListMms.put(id, msg); } while (c.moveToNext()); } } finally { if (c != null) c.close(); } synchronized(getMsgListMms()) { getMsgListMms().clear(); setMsgListMms(msgListMms, true); // Set initial folder version counter } } if(mAccount != null) { HashMap msgList = new HashMap(); Uri uri = mMessageUri; Cursor c = mProviderClient.query(uri, MSG_PROJECTION_SHORT, null, null, null); try { if (c != null && c.moveToFirst()) { do { long id = c.getLong(c.getColumnIndex(MessageColumns._ID)); long folderId = c.getInt(c.getColumnIndex( BluetoothMapContract.MessageColumns.FOLDER_ID)); int readFlag = c.getInt(c.getColumnIndex( BluetoothMapContract.MessageColumns.FLAG_READ)); Msg msg = new Msg(id, folderId, readFlag); msgList.put(id, msg); } while (c.moveToNext()); } } finally { if (c != null) c.close(); } synchronized(getMsgListMsg()) { getMsgListMsg().clear(); setMsgListMsg(msgList, true); } } } private void initContactsList() throws RemoteException { if (V) Log.d(TAG, "initContactsList"); if(mContactUri == null) { if (D) Log.d(TAG, "initContactsList() no mContactUri - nothing to init"); return; } Uri uri = mContactUri; Cursor c = mProviderClient.query(uri, BluetoothMapContract.BT_CONTACT_CHATSTATE_PRESENCE_PROJECTION, null, null, null); Map contactList = new HashMap(); try { if (c != null && c.moveToFirst()) { ConvoContactInfo cInfo = new ConvoContactInfo(); cInfo.setConvoColunms(c); do { long convoId = c.getLong(cInfo.mContactColConvoId); if (convoId == 0) continue; if (V) BluetoothMapUtils.printCursor(c); String uci = c.getString(cInfo.mContactColUci); String name = c.getString(cInfo.mContactColName); String displayName = c.getString(cInfo.mContactColNickname); String presenceStatus = c.getString(cInfo.mContactColPresenceText); int presenceState = c.getInt(cInfo.mContactColPresenceState); long lastActivity = c.getLong(cInfo.mContactColLastActive); int chatState = c.getInt(cInfo.mContactColChatState); int priority = c.getInt(cInfo.mContactColPriority); String btUid = c.getString(cInfo.mContactColBtUid); BluetoothMapConvoContactElement contact = new BluetoothMapConvoContactElement(uci, name, displayName, presenceStatus, presenceState, lastActivity, chatState, priority, btUid); contactList.put(uci, contact); } while (c.moveToNext()); } } finally { if (c != null) c.close(); } synchronized(getContactList()) { getContactList().clear(); setContactList(contactList, true); } } private void handleMsgListChangesSms() { if (V) Log.d(TAG, "handleMsgListChangesSms"); HashMap msgListSms = new HashMap(); boolean listChanged = false; Cursor c; synchronized(getMsgListSms()) { if (mMapEventReportVersion == BluetoothMapUtils.MAP_EVENT_REPORT_V10) { c = mResolver.query(Sms.CONTENT_URI, SMS_PROJECTION_SHORT, null, null, null); } else { c = mResolver.query(Sms.CONTENT_URI, SMS_PROJECTION_SHORT_EXT, null, null, null); } try { 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)); int read = c.getInt(c.getColumnIndex(Sms.READ)); Msg msg = getMsgListSms().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, threadId, read); msgListSms.put(id, msg); listChanged = true; Event evt; if (mTransmitEvents && // extract contact details only if needed mMapEventReportVersion > BluetoothMapUtils.MAP_EVENT_REPORT_V10) { String date = BluetoothMapUtils.getDateTimeString( c.getLong(c.getColumnIndex(Sms.DATE))); String subject = c.getString(c.getColumnIndex(Sms.BODY)); String name = ""; String phone = ""; if (type == 1) { //inbox phone = c.getString(c.getColumnIndex(Sms.ADDRESS)); if (phone != null && !phone.isEmpty()) { name = BluetoothMapContent.getContactNameFromPhone(phone, mResolver); if(name == null || name.isEmpty()){ name = phone; } }else{ name = phone; } } else { TelephonyManager tm = (TelephonyManager)mContext.getSystemService( Context.TELEPHONY_SERVICE); if (tm != null) { phone = tm.getLine1Number(); name = tm.getLine1AlphaTag(); if(name == null || name.isEmpty()){ name = phone; } } } String priority = "no";// no priority for sms /* Incoming message from the network */ if (mMapEventReportVersion == BluetoothMapUtils.MAP_EVENT_REPORT_V11) { evt = new Event(EVENT_TYPE_NEW, id, getSmsFolderName(type), mSmsType, date, subject, name, priority); } else { evt = new Event(EVENT_TYPE_NEW, id, getSmsFolderName(type), mSmsType, date, subject, name, priority, (long)threadId, null); } } else { /* Incoming message from the network */ evt = new Event(EVENT_TYPE_NEW, id, getSmsFolderName(type), null, mSmsType); } sendEvent(evt); } else { /* Existing message */ if (type != msg.type) { listChanged = true; Log.d(TAG, "new type: " + type + " old type: " + msg.type); String oldFolder = getSmsFolderName(msg.type); String newFolder = getSmsFolderName(type); // Filter out the intermediate outbox steps if(!oldFolder.equalsIgnoreCase(newFolder)) { Event evt = new Event(EVENT_TYPE_SHIFT, id, getSmsFolderName(type), oldFolder, mSmsType); sendEvent(evt); } msg.type = type; } else if(threadId != msg.threadId) { listChanged = true; 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 // TODO: // We shall only use the folder attribute, but can't remember // wether to set it to "deleted" or the name of the folder // from which the message have been deleted. // "old_folder" used only for MessageShift event Event evt = new Event(EVENT_TYPE_DELETE, id, getSmsFolderName(msg.type), null, mSmsType); sendEvent(evt); msg.threadId = threadId; } else { // Undelete Event evt = new Event(EVENT_TYPE_SHIFT, id, getSmsFolderName(msg.type), BluetoothMapContract.FOLDER_NAME_DELETED, mSmsType); sendEvent(evt); msg.threadId = threadId; } } if(read != msg.flagRead) { listChanged = true; msg.flagRead = read; if (mMapEventReportVersion > BluetoothMapUtils.MAP_EVENT_REPORT_V10) { Event evt = new Event(EVENT_TYPE_READ_STATUS, id, getSmsFolderName(msg.type), mSmsType); sendEvent(evt); } } msgListSms.put(id, msg); } } while (c.moveToNext()); } } finally { if (c != null) c.close(); } for (Msg msg : getMsgListSms().values()) { // "old_folder" used only for MessageShift event Event evt = new Event(EVENT_TYPE_DELETE, msg.id, getSmsFolderName(msg.type), null, mSmsType); sendEvent(evt); listChanged = true; } setMsgListSms(msgListSms, listChanged); } } private void handleMsgListChangesMms() { if (V) Log.d(TAG, "handleMsgListChangesMms"); HashMap msgListMms = new HashMap(); boolean listChanged = false; Cursor c; synchronized(getMsgListMms()) { if (mMapEventReportVersion == BluetoothMapUtils.MAP_EVENT_REPORT_V10) { c = mResolver.query(Mms.CONTENT_URI, MMS_PROJECTION_SHORT, null, null, null); } else { c = mResolver.query(Mms.CONTENT_URI, MMS_PROJECTION_SHORT_EXT, null, null, null); } try{ if (c != null && c.moveToFirst()) { do { 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)); // TODO: Go through code to see if we have an issue with mismatch in types // for threadId. Seems to be a long in DB?? int read = c.getInt(c.getColumnIndex(Mms.READ)); Msg msg = getMsgListMms().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 */ listChanged = true; if (getMmsFolderName(type).equalsIgnoreCase( BluetoothMapContract.FOLDER_NAME_INBOX) && mtype != MESSAGE_TYPE_RETRIEVE_CONF) { continue; } msg = new Msg(id, type, threadId, read); msgListMms.put(id, msg); Event evt; if (mTransmitEvents && // extract contact details only if needed mMapEventReportVersion != BluetoothMapUtils.MAP_EVENT_REPORT_V10) { String date = BluetoothMapUtils.getDateTimeString( c.getLong(c.getColumnIndex(Mms.DATE))); String subject = c.getString(c.getColumnIndex(Mms.SUBJECT)); if (subject == null || subject.length() == 0) { /* Get subject from mms text body parts - if any exists */ subject = BluetoothMapContent.getTextPartsMms(mResolver, id); } int tmpPri = c.getInt(c.getColumnIndex(Mms.PRIORITY)); Log.d(TAG, "TEMP handleMsgListChangesMms, " + "newMessage 'read' state: " + read + "priority: " + tmpPri); String address = BluetoothMapContent.getAddressMms( mResolver,id,BluetoothMapContent.MMS_FROM); String priority = "no"; if(tmpPri == PduHeaders.PRIORITY_HIGH) priority = "yes"; /* Incoming message from the network */ if (mMapEventReportVersion == BluetoothMapUtils.MAP_EVENT_REPORT_V11) { evt = new Event(EVENT_TYPE_NEW, id, getMmsFolderName(type), TYPE.MMS, date, subject, address, priority); } else { evt = new Event(EVENT_TYPE_NEW, id, getMmsFolderName(type), TYPE.MMS, date, subject, address, priority, (long)threadId, null); } } else { /* Incoming message from the network */ evt = new Event(EVENT_TYPE_NEW, id, getMmsFolderName(type), null, TYPE.MMS); } sendEvent(evt); } else { /* Existing message */ if (type != msg.type) { Log.d(TAG, "new type: " + type + " old type: " + msg.type); Event evt; listChanged = true; if(!msg.localInitiatedSend) { // Only send events about local initiated changes evt = new Event(EVENT_TYPE_SHIFT, id, getMmsFolderName(type), getMmsFolderName(msg.type), TYPE.MMS); sendEvent(evt); } msg.type = type; if (getMmsFolderName(type).equalsIgnoreCase( BluetoothMapContract.FOLDER_NAME_SENT) && msg.localInitiatedSend) { // Stop tracking changes for this message msg.localInitiatedSend = false; evt = new Event(EVENT_TYPE_SENDING_SUCCESS, id, getMmsFolderName(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); listChanged = true; if(threadId == DELETED_THREAD_ID) { // Message deleted // "old_folder" used only for MessageShift event Event evt = new Event(EVENT_TYPE_DELETE, id, getMmsFolderName(msg.type), null, TYPE.MMS); sendEvent(evt); msg.threadId = threadId; } else { // Undelete Event evt = new Event(EVENT_TYPE_SHIFT, id, getMmsFolderName(msg.type), BluetoothMapContract.FOLDER_NAME_DELETED, TYPE.MMS); sendEvent(evt); msg.threadId = threadId; } } if(read != msg.flagRead) { listChanged = true; msg.flagRead = read; if (mMapEventReportVersion > BluetoothMapUtils.MAP_EVENT_REPORT_V10) { Event evt = new Event(EVENT_TYPE_READ_STATUS, id, getMmsFolderName(msg.type), TYPE.MMS); sendEvent(evt); } } msgListMms.put(id, msg); } } while (c.moveToNext()); } } finally { if (c != null) c.close(); } for (Msg msg : getMsgListMms().values()) { // "old_folder" used only for MessageShift event Event evt = new Event(EVENT_TYPE_DELETE, msg.id, getMmsFolderName(msg.type), null, TYPE.MMS); sendEvent(evt); listChanged = true; } setMsgListMms(msgListMms, listChanged); } } private void handleMsgListChangesMsg(Uri uri) throws RemoteException{ if (V) Log.v(TAG, "handleMsgListChangesMsg uri: " + uri.toString()); // TODO: Change observer to handle accountId and message ID if present HashMap msgList = new HashMap(); Cursor c; boolean listChanged = false; if (mMapEventReportVersion == BluetoothMapUtils.MAP_EVENT_REPORT_V10) { c = mProviderClient.query(mMessageUri, MSG_PROJECTION_SHORT, null, null, null); } else if (mMapEventReportVersion == BluetoothMapUtils.MAP_EVENT_REPORT_V11) { c = mProviderClient.query(mMessageUri, MSG_PROJECTION_SHORT_EXT, null, null, null); } else { c = mProviderClient.query(mMessageUri, MSG_PROJECTION_SHORT_EXT2, null, null, null); } synchronized(getMsgListMsg()) { try { 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)); int readFlag = c.getInt(c.getColumnIndex( BluetoothMapContract.MessageColumns.FLAG_READ)); Msg msg = getMsgListMsg().remove(id); BluetoothMapFolderElement folderElement = mFolders.getFolderById(folderId); String newFolder; if(folderElement != null) { newFolder = folderElement.getFullPath(); } else { // This can happen if a new folder is created while connected newFolder = "unknown"; } /* 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) { listChanged = true; /* New message - created with message unread */ msg = new Msg(id, folderId, 0, readFlag); msgList.put(id, msg); Event evt; /* Incoming message from the network */ if (mMapEventReportVersion != BluetoothMapUtils.MAP_EVENT_REPORT_V10) { String date = BluetoothMapUtils.getDateTimeString( c.getLong(c.getColumnIndex( BluetoothMapContract.MessageColumns.DATE))); String subject = c.getString(c.getColumnIndex( BluetoothMapContract.MessageColumns.SUBJECT)); String address = c.getString(c.getColumnIndex( BluetoothMapContract.MessageColumns.FROM_LIST)); String priority = "no"; if(c.getInt(c.getColumnIndex( BluetoothMapContract.MessageColumns.FLAG_HIGH_PRIORITY)) == 1) priority = "yes"; if (mMapEventReportVersion == BluetoothMapUtils.MAP_EVENT_REPORT_V11) { evt = new Event(EVENT_TYPE_NEW, id, newFolder, mAccount.getType(), date, subject, address, priority); } else { long thread_id = c.getLong(c.getColumnIndex( BluetoothMapContract.MessageColumns.THREAD_ID)); String thread_name = c.getString(c.getColumnIndex( BluetoothMapContract.MessageColumns.THREAD_NAME)); evt = new Event(EVENT_TYPE_NEW, id, newFolder, mAccount.getType(), date, subject, address, priority, thread_id, thread_name); } } else { evt = new Event(EVENT_TYPE_NEW, id, newFolder, null, TYPE.EMAIL); } sendEvent(evt); } else { /* Existing message */ if (folderId != msg.folderId && msg.folderId != -1) { if (D) Log.d(TAG, "new folderId: " + folderId + " old folderId: " + msg.folderId); BluetoothMapFolderElement oldFolderElement = mFolders.getFolderById(msg.folderId); String oldFolder; listChanged = true; if(oldFolderElement != null) { oldFolder = oldFolderElement.getFullPath(); } else { // This can happen if a new folder is created while connected oldFolder = "unknown"; } BluetoothMapFolderElement deletedFolder = mFolders.getFolderByName( BluetoothMapContract.FOLDER_NAME_DELETED); BluetoothMapFolderElement sentFolder = mFolders.getFolderByName( 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.getFolderId() == folderId) { // "old_folder" used only for MessageShift event Event evt = new Event(EVENT_TYPE_DELETE, msg.id, oldFolder, null, mAccount.getType()); sendEvent(evt); } else if(sentFolder != null && sentFolder.getFolderId() == folderId && msg.localInitiatedSend) { 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, mAccount.getType()); sendEvent(evt); } } else { if (!oldFolder.equalsIgnoreCase("root")) { Event evt = new Event(EVENT_TYPE_SHIFT, id, newFolder, oldFolder, mAccount.getType()); sendEvent(evt); } } msg.folderId = folderId; } if(readFlag != msg.flagRead) { listChanged = true; if (mMapEventReportVersion > BluetoothMapUtils.MAP_EVENT_REPORT_V10) { Event evt = new Event(EVENT_TYPE_READ_STATUS, id, newFolder, mAccount.getType()); sendEvent(evt); msg.flagRead = readFlag; } } msgList.put(id, msg); } } while (c.moveToNext()); } } finally { if (c != null) c.close(); } // For all messages no longer in the database send a delete notification for (Msg msg : getMsgListMsg().values()) { BluetoothMapFolderElement oldFolderElement = mFolders.getFolderById(msg.folderId); String oldFolder; listChanged = true; 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) { 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, mAccount.getType()); 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) { // "old_folder" used only for MessageShift event Event evt = new Event(EVENT_TYPE_DELETE, msg.id, oldFolder, null, mAccount.getType()); sendEvent(evt); } } setMsgListMsg(msgList, listChanged); } } private void handleMsgListChanges(Uri uri) { if(uri.getAuthority().equals(mAuthority)) { try { if(D) Log.d(TAG, "handleMsgListChanges: account type = " + mAccount.getType().toString()); handleMsgListChangesMsg(uri); } catch(RemoteException e) { mMasInstance.restartObexServerSession(); Log.w(TAG, "Problems contacting the ContentProvider in mas Instance " + mMasId + " restaring ObexServerSession"); } } // TODO: check to see if there could be problem with IM and SMS in one instance if (mEnableSmsMms) { handleMsgListChangesSms(); handleMsgListChangesMms(); } } private void handleContactListChanges(Uri uri) { if (uri.getAuthority().equals(mAuthority)) { try { if (V) Log.v(TAG,"handleContactListChanges uri: " + uri.toString()); Cursor c = null; boolean listChanged = false; try { ConvoContactInfo cInfo = new ConvoContactInfo(); if (mMapEventReportVersion != BluetoothMapUtils.MAP_EVENT_REPORT_V10 && mMapEventReportVersion != BluetoothMapUtils.MAP_EVENT_REPORT_V11) { c = mProviderClient .query(mContactUri, BluetoothMapContract. BT_CONTACT_CHATSTATE_PRESENCE_PROJECTION, null, null, null); cInfo.setConvoColunms(c); } else { if (V) Log.v(TAG,"handleContactListChanges MAP version does not" + "support convocontact notifications"); return; } HashMap contactList = new HashMap(getContactList().size()); synchronized (getContactList()) { if (c != null && c.moveToFirst()) { do { String uci = c.getString(cInfo.mContactColUci); long convoId = c.getLong(cInfo.mContactColConvoId); if (convoId == 0) continue; if (V) BluetoothMapUtils.printCursor(c); BluetoothMapConvoContactElement contact = getContactList().remove(uci); /* * 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 (contact == null) { listChanged = true; /* * New contact - added to conversation and * tracked here */ if (mMapEventReportVersion != BluetoothMapUtils.MAP_EVENT_REPORT_V10 && mMapEventReportVersion != BluetoothMapUtils.MAP_EVENT_REPORT_V11) { Event evt; String name = c .getString(cInfo.mContactColName); String displayName = c .getString(cInfo.mContactColNickname); String presenceStatus = c .getString(cInfo.mContactColPresenceText); int presenceState = c .getInt(cInfo.mContactColPresenceState); long lastActivity = c .getLong(cInfo.mContactColLastActive); int chatState = c .getInt(cInfo.mContactColChatState); int priority = c .getInt(cInfo.mContactColPriority); String btUid = c .getString(cInfo.mContactColBtUid); // Get Conversation information for // event // Uri convoUri = Uri // .parse(mAccount.mBase_uri // + "/" // + BluetoothMapContract.TABLE_CONVERSATION); // String whereClause = "contacts._id = " // + convoId; // Cursor cConvo = mProviderClient // .query(convoUri, // BluetoothMapContract.BT_CONVERSATION_PROJECTION, // whereClause, null, null); // TODO: will move out of the loop when merged with CB's // changes make sure to look up col index out side loop String convoName = null; // if (cConvo != null // && cConvo.moveToFirst()) { // convoName = cConvo // .getString(cConvo // .getColumnIndex(BluetoothMapContract.ConvoContactColumns.NAME)); // } contact = new BluetoothMapConvoContactElement( uci, name, displayName, presenceStatus, presenceState, lastActivity, chatState, priority, btUid); contactList.put(uci, contact); evt = new Event( EVENT_TYPE_CONVERSATION, uci, mAccount.getType(), name, String.valueOf(priority), BluetoothMapUtils .getDateTimeString(lastActivity), convoId, convoName, presenceState, presenceStatus, chatState); sendEvent(evt); } } else { // Not new - compare updated content // Uri convoUri = Uri // .parse(mAccount.mBase_uri // + "/" // + BluetoothMapContract.TABLE_CONVERSATION); // TODO: Should be changed to own provider interface name // String whereClause = "contacts._id = " // + convoId; // Cursor cConvo = mProviderClient // .query(convoUri, // BluetoothMapContract.BT_CONVERSATION_PROJECTION, // whereClause, null, null); // // TODO: will move out of the loop when merged with CB's // // changes make sure to look up col index out side loop String convoName = null; // if (cConvo != null && cConvo.moveToFirst()) { // convoName = cConvo // .getString(cConvo // .getColumnIndex(BluetoothMapContract.ConvoContactColumns.NAME)); // } // Check if presence is updated int presenceState = c.getInt(cInfo.mContactColPresenceState); String presenceStatus = c.getString( cInfo.mContactColPresenceText); String currentPresenceStatus = contact .getPresenceStatus(); if (contact.getPresenceAvailability() != presenceState || !Objects.equals(currentPresenceStatus, presenceStatus)) { long lastOnline = c .getLong(cInfo.mContactColLastOnline); contact.setPresenceAvailability(presenceState); contact.setLastActivity(lastOnline); if (currentPresenceStatus != null && !currentPresenceStatus .equals(presenceStatus)) { contact.setPresenceStatus(presenceStatus); } Event evt = new Event( EVENT_TYPE_PRESENCE, uci, mAccount.getType(), contact.getName(), String.valueOf(contact .getPriority()), BluetoothMapUtils .getDateTimeString(lastOnline), convoId, convoName, presenceState, presenceStatus, 0); sendEvent(evt); } // Check if chat state is updated int chatState = c.getInt(cInfo.mContactColChatState); if (contact.getChatState() != chatState) { // Get DB timestamp long lastActivity = c.getLong(cInfo.mContactColLastActive); contact.setLastActivity(lastActivity); contact.setChatState(chatState); Event evt = new Event( EVENT_TYPE_CHAT_STATE, uci, mAccount.getType(), contact.getName(), String.valueOf(contact .getPriority()), BluetoothMapUtils .getDateTimeString(lastActivity), convoId, convoName, 0, null, chatState); sendEvent(evt); } contactList.put(uci, contact); } } while (c.moveToNext()); } if(getContactList().size() > 0) { // one or more contacts were deleted, hence the conversation listing // version counter should change. listChanged = true; } setContactList(contactList, listChanged); } // end synchronized } finally { if (c != null) c.close(); } } catch (RemoteException e) { mMasInstance.restartObexServerSession(); Log.w(TAG, "Problems contacting the ContentProvider in mas Instance " + mMasId + " restaring ObexServerSession"); } } // TODO: conversation contact updates if IM and SMS(MMS in one instance } 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. getFolderByName(BluetoothMapContract.FOLDER_NAME_DELETED); contentValues.put(BluetoothMapContract.MessageColumns._ID, handle); synchronized(getMsgListMsg()) { Msg msg = getMsgListMsg().get(handle); if (status == BluetoothMapAppParams.STATUS_VALUE_YES) { /* Set deleted folder id */ long folderId = -1; if(deleteFolder != null) { folderId = deleteFolder.getFolderId(); } 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.getFolderId()) { /* Only modify messages in the 'Deleted' folder */ long folderId = -1; BluetoothMapFolderElement inboxFolder = mCurrentFolder. getFolderByName(BluetoothMapContract.FOLDER_NAME_INBOX); if (msg != null && msg.oldFolderId != -1) { folderId = msg.oldFolderId; } else { if(inboxFolder != null) { folderId = inboxFolder.getFolderId(); } 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. */ /* UPDATE: Actually the BT-Spec. states that an undelete is a move of the * message to INBOX - clearified in errata 5591. * Therefore we update the cache to INBOX-folderId - to trigger a message * shift event to the old-folder. */ if(inboxFolder != null) { msg.folderId = inboxFolder.getFolderId(); } else { 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.getFolderById(msg.folderId); if(folderElement != null) { folderName = folderElement.getName(); } } Log.d(TAG,"setEmailMessageStatusDelete: " + handle + " from " + folderName + " status: " + status); } } if(!res) { 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) { boolean res = false; Uri uri = ContentUris.withAppendedId(Mms.CONTENT_URI, handle); Cursor c = mResolver.query(uri, null, null, null, null); try { if (c != null && c.moveToFirst()) { /* Move to deleted folder, or delete if already in deleted folder */ int threadId = c.getInt(c.getColumnIndex(Mms.THREAD_ID)); if (threadId != DELETED_THREAD_ID) { /* Set deleted thread id */ synchronized(getMsgListMms()) { Msg msg = getMsgListMms().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 */ synchronized(getMsgListMms()) { getMsgListMms().remove(handle); } /* Delete message */ mResolver.delete(uri, null, null); } res = true; } } finally { if (c != null) c.close(); } return res; } private boolean unDeleteMessageMms(long handle) { boolean res = false; Uri uri = ContentUris.withAppendedId(Mms.CONTENT_URI, handle); Cursor c = mResolver.query(uri, null, null, null, null); try { if (c != null && c.moveToFirst()) { int threadId = c.getInt(c.getColumnIndex(Mms.THREAD_ID)); if (threadId == DELETED_THREAD_ID) { /* 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(Mms._ID)); int msgBox = c.getInt(c.getColumnIndex(Mms.MESSAGE_BOX)); if (msgBox == Mms.MESSAGE_BOX_INBOX) { address = BluetoothMapContent.getAddressMms(mResolver, id, BluetoothMapContent.MMS_FROM); } else { address = BluetoothMapContent.getAddressMms(mResolver, id, BluetoothMapContent.MMS_TO); } Set recipients = new HashSet(); recipients.addAll(Arrays.asList(address)); Long oldThreadId = Telephony.Threads.getOrCreateThreadId(mContext, recipients); synchronized(getMsgListMms()) { Msg msg = getMsgListMms().get(handle); if(msg != null) { // This will always be the case msg.threadId = oldThreadId.intValue(); // Spec. states that undelete shall shift the message to Inbox. // Hence we need to trigger a message shift from INBOX to old-folder // after undelete. // We do this by changing the cached folder value to being inbox - hence // the event handler will se the update as the message have been shifted // from INBOX to old-folder. (Errata 5591 clearifies this) msg.type = Mms.MESSAGE_BOX_INBOX; } } updateThreadId(uri, Mms.THREAD_ID, oldThreadId); } else { Log.d(TAG, "Message not in deleted folder: handle " + handle + " threadId " + threadId); } res = true; } } finally { if (c != null) c.close(); } return res; } private boolean deleteMessageSms(long handle) { boolean res = false; Uri uri = ContentUris.withAppendedId(Sms.CONTENT_URI, handle); Cursor c = mResolver.query(uri, null, null, null, null); try { if (c != null && c.moveToFirst()) { /* 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(getMsgListSms()) { Msg msg = getMsgListSms().get(handle); if(msg != null) { // This will always be the case msg.threadId = DELETED_THREAD_ID; } } /* Set deleted thread id */ updateThreadId(uri, Sms.THREAD_ID, DELETED_THREAD_ID); } else { /* Delete from observer message list to avoid delete notifications */ synchronized(getMsgListSms()) { getMsgListSms().remove(handle); } /* Delete message */ mResolver.delete(uri, null, null); } res = true; } } finally { if (c != null) c.close(); } return res; } private boolean unDeleteMessageSms(long handle) { boolean res = false; Uri uri = ContentUris.withAppendedId(Sms.CONTENT_URI, handle); Cursor c = mResolver.query(uri, null, null, null, null); try { if (c != null && c.moveToFirst()) { int threadId = c.getInt(c.getColumnIndex(Sms.THREAD_ID)); if (threadId == DELETED_THREAD_ID) { String address = c.getString(c.getColumnIndex(Sms.ADDRESS)); Set recipients = new HashSet(); recipients.addAll(Arrays.asList(address)); Long oldThreadId = Telephony.Threads.getOrCreateThreadId(mContext, recipients); synchronized(getMsgListSms()) { Msg msg = getMsgListSms().get(handle); if(msg != null) { msg.threadId = oldThreadId.intValue(); /* This will always be the case * The threadId is specified as an int, so it is safe to truncate * TODO: Test that this will trigger a message-shift from Inbox * to old-folder **/ /* Spec. states that undelete shall shift the message to Inbox. * Hence we need to trigger a message shift from INBOX to old-folder * after undelete. * We do this by changing the cached folder value to being inbox - hence * the event handler will se the update as the message have been shifted * from INBOX to old-folder. (Errata 5591 clearifies this) * */ msg.type = Sms.MESSAGE_TYPE_INBOX; } } updateThreadId(uri, Sms.THREAD_ID, oldThreadId); } else { Log.d(TAG, "Message not in deleted folder: handle " + handle + " threadId " + threadId); } res = true; } } finally { if (c != null) c.close(); } return res; } /** * * @param handle * @param type * @param mCurrentFolder * @param uriStr * @param statusValue * @return true is success */ 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 (type == TYPE.EMAIL) { res = setEmailMessageStatusDelete(mCurrentFolder, uriStr, handle, statusValue); } else if (type == TYPE.IM) { // TODO: to do when deleting IM message if (D) Log.d(TAG, "setMessageStatusDeleted: IM not handled" ); } 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; } /** * * @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); /* Approved MAP spec errata 3445 states that read status initiated * 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); ContentValues contentValues = new ContentValues(); contentValues.put(Sms.READ, statusValue); contentValues.put(Sms.SEEN, statusValue); String values = contentValues.toString(); if (D) Log.d(TAG, " -> SMS Uri: " + uri.toString() + " values " + values); synchronized(getMsgListSms()) { Msg msg = getMsgListSms().get(handle); if(msg != null) { // This will always be the case msg.flagRead = statusValue; } } count = mResolver.update(uri, contentValues, null, null); if (D) Log.d(TAG, " -> "+count +" rows updated!"); } else if (type == TYPE.MMS) { Uri uri = ContentUris.withAppendedId(Mms.CONTENT_URI, handle); if (D) Log.d(TAG, " -> MMS Uri: " + uri.toString()); ContentValues contentValues = new ContentValues(); contentValues.put(Mms.READ, statusValue); synchronized(getMsgListMms()) { Msg msg = getMsgListMms().get(handle); if(msg != null) { // This will always be the case msg.flagRead = statusValue; } } count = mResolver.update(uri, contentValues, null, null); if (D) Log.d(TAG, " -> "+count +" rows updated!"); } else if (type == TYPE.EMAIL || type == TYPE.IM) { Uri uri = mMessageUri; ContentValues contentValues = new ContentValues(); contentValues.put(BluetoothMapContract.MessageColumns.FLAG_READ, statusValue); contentValues.put(BluetoothMapContract.MessageColumns._ID, handle); synchronized(getMsgListMsg()) { Msg msg = getMsgListMsg().get(handle); if(msg != null) { // This will always be the case msg.flagRead = statusValue; } } count = mProviderClient.update(uri, contentValues, null, null); } return (count > 0); } private class PushMsgInfo { long id; int transparent; 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. PushMsgInfo(long id, int transparent, int retry, String phone, Uri uri) { this.id = id; this.transparent = transparent; this.retry = retry; 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 mPushMsgList = Collections.synchronizedMap(new HashMap()); public long pushMessage(BluetoothMapbMessage msg, BluetoothMapFolderElement folderElement, BluetoothMapAppParams ap, String emailBaseUri) throws IllegalArgumentException, RemoteException, IOException { if (D) Log.d(TAG, "pushMessage"); ArrayList recipientList = msg.getRecipients(); int transparent = (ap.getTransparent() == BluetoothMapAppParams.INVALID_VALUE_PARAMETER) ? 0 : ap.getTransparent(); int retry = ap.getRetry(); int charset = ap.getCharset(); long handle = -1; long folderId = -1; if (recipientList == null) { if (folderElement.getName().equalsIgnoreCase(BluetoothMapContract.FOLDER_NAME_DRAFT)) { BluetoothMapbMessage.VCard empty = new BluetoothMapbMessage.VCard("", "", null, null, 0); recipientList = new ArrayList(); recipientList.add(empty); Log.w(TAG, "Added empty recipient to draft message"); } else { Log.e(TAG, "Trying to send a message with no recipients"); return -1; } } 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.getFolderId()); synchronized(getMsgListMsg()) { // Now insert the empty message into folder ContentValues values = new ContentValues(); folderId = folderElement.getFolderId(); 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. */ /*TODO: We need to add the new 1.1 parameter as well:-) e.g. read*/ Msg newMsg = new Msg(handle, folderId, 1); // TODO: Create define for read-state newMsg.transparent = transparent == 1; if ( folderId == folderElement.getFolderByName( BluetoothMapContract.FOLDER_NAME_OUTBOX).getFolderId() ) { newMsg.localInitiatedSend = true; } getMsgListMsg().put(handle, newMsg); } } else { // type SMS_* of MMS for (BluetoothMapbMessage.VCard recipient : recipientList) { // Only send the message to the top level recipient if(recipient.getEnvLevel() == 0) { /* 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) && (((BluetoothMapbMessageMime) msg).getTextOnly())) { msgBody = ((BluetoothMapbMessageMime) msg).getMessageAsText(); SmsManager smsMng = SmsManager.getDefault(); ArrayList 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; } } if (msg.getType().equals(TYPE.MMS)) { /* Send message if folder is outbox else just store in draft*/ handle = sendMmsMessage(folder, phone, (BluetoothMapbMessageMime)msg, transparent, retry); } else if (msg.getType().equals(TYPE.SMS_GSM) || msg.getType().equals(TYPE.SMS_CDMA) ) { /* Add the message to the database */ if(msgBody == null) msgBody = ((BluetoothMapbMessageSms) msg).getSmsBody(); if (TextUtils.isEmpty(msgBody)) { Log.d(TAG, "PushMsg: Empty msgBody "); /* not allowed to push empty message */ throw new IllegalArgumentException("push EMPTY message: Invalid Body"); } /* 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(getMsgListSms()) { 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. */ try { 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)); int readFlag = c.getInt(c.getColumnIndex(Sms.READ)); if(V) Log.v(TAG, "add message with id=" + id + " type=" + type + " threadId=" + threadId + " readFlag=" + readFlag + "to mMsgListSms"); Msg newMsg = new Msg(id, type, threadId, readFlag); getMsgListSms().put(id, newMsg); c.close(); } else { Log.w(TAG,"Message: " + uri + " no longer exist!"); /* This can only happen, if the message is deleted * just as it is added */ return -1; } } finally { if (c != null) c.close(); } handle = Long.parseLong(uri.getLastPathSegment()); /* Send message if folder is outbox */ if (folder.equalsIgnoreCase(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..."); } /* else just added to draft */ /* sendMessage causes the message to be deleted and reinserted, * hence we need to lock the list while this is happening. */ } } else { if (D) Log.d(TAG, "pushMessage - failure on type " ); return -1; } } } } /* If multiple recipients return handle of last */ return handle; } public long sendMmsMessage(String folder, String toAddress, BluetoothMapbMessageMime msg, int transparent, int retry) { /* *strategy: *1) parse message into parts *if folder is outbox/drafts: *2) push message to draft *if folder is outbox: *3) move message to outbox (to trigger the mms app to add msg to pending_messages list) *4) send intent to mms app in order to wake it up. *else if folder !outbox: *1) push message to folder * */ if (folder != null && (folder.equalsIgnoreCase(BluetoothMapContract.FOLDER_NAME_OUTBOX) || folder.equalsIgnoreCase(BluetoothMapContract.FOLDER_NAME_DRAFT))) { long handle = pushMmsToFolder(Mms.MESSAGE_BOX_DRAFTS, toAddress, 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)) { Uri btMmsUri = MmsFileProvider.CONTENT_URI.buildUpon() .appendPath(Long.toString(handle)).build(); Intent sentIntent = new Intent(ACTION_MESSAGE_SENT); // TODO: update the mmsMsgList <- done in pushMmsToFolder() but check sentIntent.setType("message/" + Long.toString(handle)); sentIntent.putExtra(EXTRA_MESSAGE_SENT_MSG_TYPE, TYPE.MMS.ordinal()); sentIntent.putExtra(EXTRA_MESSAGE_SENT_HANDLE, handle); // needed for notification sentIntent.putExtra(EXTRA_MESSAGE_SENT_TRANSPARENT, transparent); sentIntent.putExtra(EXTRA_MESSAGE_SENT_RETRY, retry); //sentIntent.setDataAndNormalize(btMmsUri); PendingIntent pendingSendIntent = PendingIntent.getBroadcast(mContext, 0, sentIntent, 0); SmsManager.getDefault().sendMultimediaMessage(mContext, btMmsUri, null/*locationUrl*/, null/*configOverrides*/, pendingSendIntent); } 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"); } } private void moveDraftToOutbox(long handle) { moveMmsToFolder(handle, mResolver, Mms.MESSAGE_BOX_OUTBOX); } /** * Move a MMS to another folder. * @param handle the CP handle of the message to move * @param resolver the ContentResolver to use * @param folder the destination folder - use Mms.MESSAGE_BOX_xxx */ private static void moveMmsToFolder(long handle, ContentResolver resolver, int folder) { /*Move message by changing the msg_box value in the content provider database */ if (handle != -1) { String whereClause = " _id= " + handle; Uri uri = Mms.CONTENT_URI; Cursor queryResult = resolver.query(uri, null, whereClause, null, null); try { if (queryResult != null) { if (queryResult.getCount() > 0) { queryResult.moveToFirst(); ContentValues data = new ContentValues(); /* set folder to be outbox */ data.put(Mms.MESSAGE_BOX, folder); resolver.update(uri, data, whereClause, null); if (D) Log.d(TAG, "moved MMS message to " + getMmsFolderName(folder)); } } else { Log.w(TAG, "Could not move MMS message to " + getMmsFolderName(folder)); } } finally { if (queryResult != null) queryResult.close(); } } } private long pushMmsToFolder(int folder, String toAddress, BluetoothMapbMessageMime msg) { /** * strategy: * 1) parse msg into parts + header * 2) create thread id (abuse the ease of adding an SMS to get id for thread) * 3) push parts into content://mms/parts/ table * 3) */ ContentValues values = new ContentValues(); 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, ""); } 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()) values.put(Mms.TEXT_ONLY, true); values.put(Mms.MESSAGE_SIZE, msg.getSize()); // Get thread id Set recipients = new HashSet(); recipients.addAll(Arrays.asList(toAddress)); values.put(Mms.THREAD_ID, Telephony.Threads.getOrCreateThreadId(mContext, recipients)); Uri uri = Mms.CONTENT_URI; synchronized (getMsgListMms()) { 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); try { 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)); int readStatus = c.getInt(c.getColumnIndex(Mms.READ)); /* 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, readStatus); newMsg.localInitiatedSend = true; getMsgListMms().put(id, newMsg); c.close(); } } finally { if (c != null) 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()); try { 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, ""); } } // 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, ""); } 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()); } } } } catch (UnsupportedEncodingException e) { Log.w(TAG, e); } catch (IOException e) { Log.w(TAG, e); } values.clear(); 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(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(Mms.Addr.CONTACT_ID, "null"); values.put(Mms.Addr.ADDRESS, toAddress); values.put(Mms.Addr.TYPE, BluetoothMapContent.MMS_TO); values.put(Mms.Addr.CHARSET, 106); uri = Uri.parse(Mms.CONTENT_URI + "/" + handle + "/addr"); uri = mResolver.insert(uri, values); if (uri != null && V){ Log.v(TAG, " NEW URI " + uri.toString()); } return handle; } private void writeMmsDataPart(long handle, MimePart part, int count) throws IOException{ ContentValues values = new ContentValues(); 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, ""); } } 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(Mms.Part.FILENAME, "part_" + count + ".dat"); values.put(Mms.Part.NAME, "part_" + count + ".dat"); } 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.mData); os.close(); } public void sendMessage(PushMsgInfo msgInfo, String msgBody) { SmsManager smsMng = SmsManager.getDefault(); ArrayList 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 deliveryIntents = new ArrayList(msgInfo.parts); ArrayList sentIntents = new ArrayList(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. */ if (parts != null && parts.size() > 0) { for (int i = 0; i < msgInfo.parts; i++) { 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); smsMng.sendMultipartTextMessage(msgInfo.phone, null, parts, sentIntents, deliveryIntents); } } private class SmsBroadcastReceiver extends BroadcastReceiver { private final String[] ID_PROJECTION = new String[] { Sms._ID }; 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); try{ intentFilter.addDataType("message/*"); } catch (MalformedMimeTypeException e) { Log.e(TAG, "Wrong mime type!!!", e); } mContext.registerReceiver(this, intentFilter, null, handler); } public void unregister() { try { mContext.unregisterReceiver(this); } catch (IllegalArgumentException e) { /* do nothing */ } } @Override public void onReceive(Context context, Intent intent) { String action = intent.getAction(); long handle = intent.getLongExtra(EXTRA_MESSAGE_SENT_HANDLE, -1); PushMsgInfo msgInfo = mPushMsgList.get(handle); Log.d(TAG, "onReceive: action" + action); if (msgInfo == null) { Log.d(TAG, "onReceive: no msgInfo found for handle " + handle); return; } 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)) { long timestamp = intent.getLongExtra(EXTRA_MESSAGE_SENT_TIMESTAMP, 0); int status = -1; if(msgInfo.timestamp == timestamp) { msgInfo.partsDelivered++; } } else { Log.d(TAG, "onReceive: Unknown action " + action); } } 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(D) Log.d(TAG,"actionMessageSent(): msgInfo.failedSent = " + msgInfo.failedSent); msgInfo.sendInProgress = false; if (!msgInfo.failedSent) { if(D) Log.d(TAG, "actionMessageSent: result OK"); if (msgInfo.transparent == 0) { if (!Sms.moveMessageToFolder(context, msgInfo.uri, Sms.MESSAGE_TYPE_SENT, 0)) { Log.w(TAG, "Failed to move " + msgInfo.uri + " to SENT"); } } else { delete = true; } Event evt = new Event(EVENT_TYPE_SENDING_SUCCESS, msgInfo.id, getSmsFolderName(Sms.MESSAGE_TYPE_SENT), null, mSmsType); sendEvent(evt); } else { if (msgInfo.retry == 1) { /* Notify failure, but keep message in outbox for resending */ msgInfo.resend = true; msgInfo.partsSent = 0; // Reset counter for the retry msgInfo.failedSent = false; Event evt = new Event(EVENT_TYPE_SENDING_FAILURE, msgInfo.id, getSmsFolderName(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.w(TAG, "Failed to move " + msgInfo.uri + " to FAILED"); } } else { delete = true; } Event evt = new Event(EVENT_TYPE_SENDING_FAILURE, msgInfo.id, getSmsFolderName(Sms.MESSAGE_TYPE_FAILED), null, mSmsType); sendEvent(evt); } } if (delete) { /* Delete from Observer message list to avoid delete notifications */ synchronized(getMsgListSms()) { getMsgListSms().remove(msgInfo.id); } /* Delete from DB */ mResolver.delete(msgInfo.uri, null, null); } } private void actionMessageDelivery(Context context, Intent intent, PushMsgInfo msgInfo) { Uri messageUri = intent.getData(); msgInfo.sendInProgress = false; Cursor cursor = mResolver.query(msgInfo.uri, ID_PROJECTION, null, null, null); try { if (cursor.moveToFirst()) { int messageId = cursor.getInt(0); Uri updateUri = ContentUris.withAppendedId(UPDATE_STATUS_URI, messageId); if(D) Log.d(TAG, "actionMessageDelivery: uri=" + messageUri + ", status=" + msgInfo.statusDelivered); ContentValues contentValues = new ContentValues(2); contentValues.put(Sms.STATUS, msgInfo.statusDelivered); contentValues.put(Inbox.DATE_SENT, System.currentTimeMillis()); mResolver.update(updateUri, contentValues, null, null); } else { Log.d(TAG, "Can't find message for status update: " + messageUri); } } finally { if (cursor != null) cursor.close(); } if (msgInfo.statusDelivered == 0) { Event evt = new Event(EVENT_TYPE_DELEVERY_SUCCESS, msgInfo.id, getSmsFolderName(Sms.MESSAGE_TYPE_SENT), null, mSmsType); sendEvent(evt); } else { Event evt = new Event(EVENT_TYPE_DELIVERY_FAILURE, msgInfo.id, getSmsFolderName(Sms.MESSAGE_TYPE_SENT), null, mSmsType); sendEvent(evt); } mPushMsgList.remove(msgInfo.id); } } private class CeBroadcastReceiver extends BroadcastReceiver { public void register() { UserManager manager = UserManager.get(mContext); if (manager == null || manager.isUserUnlocked()) { mStorageUnlocked = true; return; } Handler handler = new Handler(Looper.getMainLooper()); IntentFilter intentFilter = new IntentFilter(); intentFilter.addAction(Intent.ACTION_BOOT_COMPLETED); mContext.registerReceiver(this, intentFilter, null, handler); } public void unregister() { try { mContext.unregisterReceiver(this); } catch (IllegalArgumentException e) { /* do nothing */ } } @Override public void onReceive(Context context, Intent intent) { String action = intent.getAction(); Log.d(TAG, "onReceive: action" + action); if (action.equals(Intent.ACTION_BOOT_COMPLETED)) { try { initMsgList(); } catch (RemoteException e) { Log.e(TAG, "Error initializing SMS/MMS message lists."); } for (String folder : FOLDER_SMS_MAP.values()) { Event evt = new Event(EVENT_TYPE_NEW, -1, folder, mSmsType); sendEvent(evt); } mStorageUnlocked = true; /* After unlock this BroadcastReceiver is never needed */ unregister(); } else { Log.d(TAG, "onReceive: Unknown action " + action); } } } /** * Handle MMS sent intents in disconnected(MNS) state, where we do not need to send any * notifications. * @param context The context to use for provider operations * @param intent The intent received * @param result The result */ public static void actionMmsSent(Context context, Intent intent, int result, Map mmsMsgList) { /* * if transparent: * delete message and send notification(regardless of result) * else * Result == Success: * move to sent folder (will trigger notification) * Result == Fail: * move to outbox (send delivery fail notification) */ if(D) Log.d(TAG,"actionMmsSent()"); int transparent = intent.getIntExtra(EXTRA_MESSAGE_SENT_TRANSPARENT, 0); long handle = intent.getLongExtra(EXTRA_MESSAGE_SENT_HANDLE, -1); if(handle < 0) { Log.w(TAG, "Intent received for an invalid handle"); return; } ContentResolver resolver = context.getContentResolver(); if(transparent == 1) { /* The specification is a bit unclear about the transparent flag. If it is set * no copy of the message shall be kept in the send folder after the message * was send, but in the case of a send error, it is unclear what to do. * As it will not be transparent if we keep the message in any folder, * we delete the message regardless of the result. * If we however do have a MNS connection we need to send a notification. */ Uri uri = ContentUris.withAppendedId(Mms.CONTENT_URI, handle); /* Delete from observer message list to avoid delete notifications */ if(mmsMsgList != null) { synchronized(mmsMsgList) { mmsMsgList.remove(handle); } } /* Delete message */ if(D) Log.d(TAG,"Transparent in use - delete"); resolver.delete(uri, null, null); } else if (result == Activity.RESULT_OK) { /* This will trigger a notification */ moveMmsToFolder(handle, resolver, Mms.MESSAGE_BOX_SENT); } else { if(mmsMsgList != null) { synchronized(mmsMsgList) { Msg msg = mmsMsgList.get(handle); if(msg != null) { msg.type=Mms.MESSAGE_BOX_OUTBOX; } } } /* Hand further retries over to the MMS application */ moveMmsToFolder(handle, resolver, Mms.MESSAGE_BOX_OUTBOX); } } public static void actionMessageSentDisconnected(Context context, Intent intent, int result) { TYPE type = TYPE.fromOrdinal( intent.getIntExtra(EXTRA_MESSAGE_SENT_MSG_TYPE, TYPE.NONE.ordinal())); if(type == TYPE.MMS) { actionMmsSent(context, intent, result, null); } else { actionSmsSentDisconnected(context, intent, result); } } public static void actionSmsSentDisconnected(Context context, Intent intent, int result) { /* Check permission for message deletion. */ if ((Binder.getCallingPid() != Process.myPid()) || (context.checkCallingOrSelfPermission("android.Manifest.permission.WRITE_SMS") != PackageManager.PERMISSION_GRANTED)) { Log.w(TAG, "actionSmsSentDisconnected: Not allowed to delete SMS/MMS messages"); return; } 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) { /* 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); } private void unRegisterPhoneServiceStateListener() { TelephonyManager tm = (TelephonyManager)mContext.getSystemService( Context.TELEPHONY_SERVICE); tm.listen(mPhoneListener, PhoneStateListener.LISTEN_NONE); } private void resendPendingMessages() { /* Send pending messages in outbox */ String where = "type = " + Sms.MESSAGE_TYPE_OUTBOX; UserManager manager = UserManager.get(mContext); if (manager == null || !manager.isUserUnlocked()) return; Cursor c = mResolver.query(Sms.CONTENT_URI, SMS_PROJECTION, where, null, null); try { if (c != null && c.moveToFirst()) { do { 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 || msgInfo.sendInProgress) { continue; } msgInfo.sendInProgress = true; sendMessage(msgInfo, msgBody); } while (c.moveToNext()); } } finally { if (c != null) c.close(); } } private void failPendingMessages() { /* Move pending messages from outbox to failed */ String where = "type = " + Sms.MESSAGE_TYPE_OUTBOX; Cursor c = mResolver.query(Sms.CONTENT_URI, SMS_PROJECTION, where, null, null); try { if (c != null && c.moveToFirst()) { do { 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) { continue; } Sms.moveMessageToFolder(mContext, msgInfo.uri, Sms.MESSAGE_TYPE_FAILED, 0); } while (c.moveToNext()); } } finally { if (c != null) c.close(); } } private void removeDeletedMessages() { /* Remove messages from virtual "deleted" folder (thread_id -1) */ mResolver.delete(Sms.CONTENT_URI, "thread_id = " + DELETED_THREAD_ID, null); } private PhoneStateListener mPhoneListener = new PhoneStateListener() { @Override public void onServiceStateChanged(ServiceState serviceState) { Log.d(TAG, "Phone service state change: " + serviceState.getState()); if (serviceState.getState() == ServiceState.STATE_IN_SERVICE) { resendPendingMessages(); } } }; public void init() { if (mSmsBroadcastReceiver != null) { mSmsBroadcastReceiver.register(); } if (mCeBroadcastReceiver != null) { mCeBroadcastReceiver.register(); } registerPhoneServiceStateListener(); mInitialized = true; } public void deinit() { mInitialized = false; unregisterObserver(); if (mSmsBroadcastReceiver != null) { mSmsBroadcastReceiver.unregister(); } unRegisterPhoneServiceStateListener(); failPendingMessages(); removeDeletedMessages(); } public boolean handleSmsSendIntent(Context context, Intent intent){ TYPE type = TYPE.fromOrdinal( intent.getIntExtra(EXTRA_MESSAGE_SENT_MSG_TYPE, TYPE.NONE.ordinal())); if(type == TYPE.MMS) { return handleMmsSendIntent(context, intent); } else { if(mInitialized) { mSmsBroadcastReceiver.onReceive(context, intent); return true; } } return false; } public boolean handleMmsSendIntent(Context context, Intent intent){ if(D) Log.w(TAG, "handleMmsSendIntent()"); if(!mMnsClient.isConnected()) { // No need to handle notifications, just use default handling if(D) Log.w(TAG, "MNS not connected - use static handling"); return false; } long handle = intent.getLongExtra(EXTRA_MESSAGE_SENT_HANDLE, -1); int result = intent.getIntExtra(EXTRA_MESSAGE_SENT_RESULT, Activity.RESULT_CANCELED); actionMmsSent(context, intent, result, getMsgListMms()); if(handle < 0) { Log.w(TAG, "Intent received for an invalid handle"); return true; } if(result != Activity.RESULT_OK) { if(mObserverRegistered) { Event evt = new Event(EVENT_TYPE_SENDING_FAILURE, handle, getMmsFolderName(Mms.MESSAGE_BOX_OUTBOX), null, TYPE.MMS); sendEvent(evt); } } else { int transparent = intent.getIntExtra(EXTRA_MESSAGE_SENT_TRANSPARENT, 0); if(transparent != 0) { if(mObserverRegistered) { Event evt = new Event(EVENT_TYPE_SENDING_SUCCESS, handle, getMmsFolderName(Mms.MESSAGE_BOX_OUTBOX), null, TYPE.MMS); sendEvent(evt); } } } return true; } }