| /* |
| * 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.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.SmsMessage; |
| import android.telephony.TelephonyManager; |
| import android.text.format.DateUtils; |
| import android.util.Log; |
| import android.util.Xml; |
| import android.text.TextUtils; |
| |
| 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.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(); |
| |
| mMapSupportedFeatures = mMasInstance.getRemoteFeatureMask(); |
| if (D) Log.d(TAG, "BluetoothMapContentObserver: Supported features " + |
| Integer.toHexString(mMapSupportedFeatures) ) ; |
| |
| 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; |
| } |
| |
| 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<String, BluetoothMapConvoContactElement>(), 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<Long, Msg>(), false); |
| doInit = true; |
| } |
| mMsgListMms = mMasInstance.getMsgListMms(); |
| if(mMsgListMms == null) { |
| setMsgListMms(new HashMap<Long, Msg>(), false); |
| doInit = true; |
| } |
| } |
| if(mAccount != null) { |
| mMsgListMsg = mMasInstance.getMsgListMsg(); |
| if(mMsgListMsg == null) { |
| setMsgListMsg(new HashMap<Long, Msg>(), 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; |
| 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; |
| } |
| if (V) Log.d(TAG, "setObserverRemoteFeatureMask : " + mMapEventReportVersion |
| + " mMapSupportedFeatures : " + mMapSupportedFeatures); |
| } |
| |
| private Map<Long, Msg> getMsgListSms() { |
| return mMsgListSms; |
| } |
| |
| private void setMsgListSms(Map<Long, Msg> msgListSms, boolean changesDetected) { |
| mMsgListSms = msgListSms; |
| if(changesDetected) { |
| mMasInstance.updateFolderVersionCounter(); |
| } |
| mMasInstance.setMsgListSms(msgListSms); |
| } |
| |
| |
| private Map<Long, Msg> getMsgListMms() { |
| return mMsgListMms; |
| } |
| |
| |
| private void setMsgListMms(Map<Long, Msg> msgListMms, boolean changesDetected) { |
| mMsgListMms = msgListMms; |
| if(changesDetected) { |
| mMasInstance.updateFolderVersionCounter(); |
| } |
| mMasInstance.setMsgListMms(msgListMms); |
| } |
| |
| |
| private Map<Long, Msg> getMsgListMsg() { |
| return mMsgListMsg; |
| } |
| |
| |
| private void setMsgListMsg(Map<Long, Msg> msgListMsg, boolean changesDetected) { |
| mMsgListMsg = msgListMsg; |
| if(changesDetected) { |
| mMasInstance.updateFolderVersionCounter(); |
| } |
| mMasInstance.setMsgListMsg(msgListMsg); |
| } |
| |
| private Map<String, BluetoothMapConvoContactElement> 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<String, BluetoothMapConvoContactElement> 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<Integer, String> FOLDER_SMS_MAP; |
| static { |
| FOLDER_SMS_MAP = new HashMap<Integer, String>(); |
| 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<Integer, String> FOLDER_MMS_MAP; |
| static { |
| FOLDER_MMS_MAP = new HashMap<Integer, String>(); |
| 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; |
| |
| final static 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; |
| } |
| } |
| |
| public 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; |
| } |
| |
| public 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 */ |
| public 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 */ |
| public 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 */ |
| public 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_V10) { |
| xmlEvtReport.attribute("", "version", BluetoothMapUtils.MAP_V10_STR); |
| } else if (mMapEventReportVersion == BluetoothMapUtils.MAP_EVENT_REPORT_V11) { |
| xmlEvtReport.attribute("", "version", BluetoothMapUtils.MAP_V11_STR); |
| } else { |
| xmlEvtReport.attribute("", "version", BluetoothMapUtils.MAP_V12_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 |
| |
| public Msg(long id, int type, int threadId, int readFlag) { |
| this.id = id; |
| this.type = type; |
| this.threadId = threadId; |
| this.flagRead = readFlag; |
| } |
| public 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<Long, Msg> mMsgListSms = null; |
| |
| private Map<Long, Msg> mMsgListMms = null; |
| |
| private Map<Long, Msg> mMsgListMsg = null; |
| |
| private Map<String, BluetoothMapConvoContactElement> 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 == false) { |
| 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() == false) { |
| 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(evt.eventType == EVENT_TYPE_NEW) { |
| if(!sendEventNewMessage(eventFilter)) { |
| if(D)Log.d(TAG, "Skip sending event of type: " + evt.eventType); |
| return; |
| } |
| } else if(evt.eventType == EVENT_TYPE_DELETE) { |
| if(!sendEventMessageDeleted(eventFilter)) { |
| if(D)Log.d(TAG, "Skip sending event of type: " + evt.eventType); |
| return; |
| } |
| } else if(evt.eventType == EVENT_TYPE_REMOVED) { |
| if(!sendEventMessageRemoved(eventFilter)) { |
| if(D)Log.d(TAG, "Skip sending event of type: " + evt.eventType); |
| return; |
| } |
| } else if(evt.eventType == EVENT_TYPE_SHIFT) { |
| if(!sendEventMessageShift(eventFilter)) { |
| if(D)Log.d(TAG, "Skip sending event of type: " + evt.eventType); |
| return; |
| } |
| } else if(evt.eventType == EVENT_TYPE_DELEVERY_SUCCESS) { |
| if(!sendEventDeliverySuccess(eventFilter)) { |
| if(D)Log.d(TAG, "Skip sending event of type: " + evt.eventType); |
| return; |
| } |
| } else if(evt.eventType == EVENT_TYPE_SENDING_SUCCESS) { |
| if(!sendEventSendingSuccess(eventFilter)) { |
| if(D)Log.d(TAG, "Skip sending event of type: " + evt.eventType); |
| return; |
| } |
| } else if(evt.eventType == EVENT_TYPE_SENDING_FAILURE) { |
| if(!sendEventSendingFailed(eventFilter)) { |
| if(D)Log.d(TAG, "Skip sending event of type: " + evt.eventType); |
| return; |
| } |
| } else if(evt.eventType == EVENT_TYPE_DELIVERY_FAILURE) { |
| if(!sendEventDeliveryFailed(eventFilter)) { |
| if(D)Log.d(TAG, "Skip sending event of type: " + evt.eventType); |
| return; |
| } |
| } else if(evt.eventType == EVENT_TYPE_READ_STATUS) { |
| if(!sendEventReadStatusChanged(eventFilter)) { |
| if(D)Log.d(TAG, "Skip sending event of type: " + evt.eventType); |
| return; |
| } |
| } else if(evt.eventType == EVENT_TYPE_CONVERSATION) { |
| if(!sendEventConversationChanged(eventFilter)) { |
| if(D)Log.d(TAG, "Skip sending event of type: " + evt.eventType); |
| return; |
| } |
| } else if(evt.eventType == EVENT_TYPE_PRESENCE) { |
| if(!sendEventParticipantPresenceChanged(eventFilter)) { |
| if(D)Log.d(TAG, "Skip sending event of type: " + evt.eventType); |
| return; |
| } |
| } else if(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<Long, Msg> msgListSms = new HashMap<Long, Msg>(); |
| |
| Cursor c = mResolver.query(Sms.CONTENT_URI, |
| SMS_PROJECTION_SHORT, 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 = 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<Long, Msg> msgListMms = new HashMap<Long, Msg>(); |
| |
| 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<Long, Msg> msgList = new HashMap<Long, Msg>(); |
| 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<String, BluetoothMapConvoContactElement> contactList = |
| new HashMap<String, BluetoothMapConvoContactElement>(); |
| 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<Long, Msg> msgListSms = new HashMap<Long, Msg>(); |
| boolean listChanged = false; |
| |
| Cursor c; |
| 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); |
| } |
| synchronized(getMsgListSms()) { |
| 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 == true && // 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<Long, Msg> msgListMms = new HashMap<Long, Msg>(); |
| boolean listChanged = false; |
| Cursor c; |
| 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); |
| } |
| |
| synchronized(getMsgListMms()) { |
| 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 == true && // 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 == false) { |
| // 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 == true) { |
| // 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<Long, Msg> msgList = new HashMap<Long, Msg>(); |
| 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 == true) { |
| if(msg.transparent) { |
| mResolver.delete( |
| ContentUris.withAppendedId(mMessageUri, id), |
| null, null); |
| } else { |
| msg.localInitiatedSend = false; |
| Event evt = new Event(EVENT_TYPE_SENDING_SUCCESS, msg.id, |
| oldFolder, null, 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 == true) { |
| msg.localInitiatedSend = false; |
| // If message is send with transparency don't set folder as message is deleted |
| if (msg.transparent) |
| oldFolder = null; |
| Event evt = new Event(EVENT_TYPE_SENDING_SUCCESS, msg.id, oldFolder, null, |
| 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<String, BluetoothMapConvoContactElement> contactList = |
| new HashMap<String, |
| BluetoothMapConvoContactElement>(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 |
| || 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 == false) { |
| Log.w(TAG, "Set delete status " + status + " failed."); |
| } |
| return res; |
| } |
| |
| private void updateThreadId(Uri uri, String valueString, long threadId) { |
| ContentValues contentValues = new ContentValues(); |
| contentValues.put(valueString, threadId); |
| mResolver.update(uri, contentValues, null, null); |
| } |
| |
| private boolean deleteMessageMms(long handle) { |
| 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<String> recipients = new HashSet<String>(); |
| 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<String> recipients = new HashSet<String>(); |
| 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. |
| |
| public 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<Long, PushMsgInfo> mPushMsgList = |
| Collections.synchronizedMap(new HashMap<Long, PushMsgInfo>()); |
| |
| public long pushMessage(BluetoothMapbMessage msg, BluetoothMapFolderElement folderElement, |
| BluetoothMapAppParams ap, String emailBaseUri) |
| throws IllegalArgumentException, RemoteException, IOException { |
| if (D) Log.d(TAG, "pushMessage"); |
| ArrayList<BluetoothMapbMessage.vCard> recipientList = msg.getRecipients(); |
| int transparent = (ap.getTransparent() == BluetoothMapAppParams.INVALID_VALUE_PARAMETER) ? |
| 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<BluetoothMapbMessage.vCard>(); |
| 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) ? true : false; |
| 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() == true)) { |
| msgBody = ((BluetoothMapbMessageMime) msg).getMessageAsText(); |
| SmsManager smsMng = SmsManager.getDefault(); |
| ArrayList<String> parts = smsMng.divideMessage(msgBody); |
| int smsParts = parts.size(); |
| if (smsParts <= CONVERT_MMS_TO_SMS_PART_COUNT ) { |
| if (D) Log.d(TAG, "pushMessage - converting MMS to SMS, sms parts=" |
| + smsParts ); |
| msg.setType(mSmsType); |
| } else { |
| if (D) Log.d(TAG, "pushMessage - MMS text only but to big to " + |
| "convert to SMS"); |
| msgBody = null; |
| } |
| |
| } |
| |
| 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 to_address, 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, to_address, msg); |
| /* if invalid handle (-1) then just return the handle |
| * - else continue sending (if folder is outbox) */ |
| if (BluetoothMapAppParams.INVALID_VALUE_PARAMETER != handle && |
| folder.equalsIgnoreCase(BluetoothMapContract.FOLDER_NAME_OUTBOX)) { |
| 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 to_address, 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() == true) |
| values.put(Mms.TEXT_ONLY, true); |
| values.put(Mms.MESSAGE_SIZE, msg.getSize()); |
| |
| // Get thread id |
| Set<String> recipients = new HashSet<String>(); |
| recipients.addAll(Arrays.asList(to_address)); |
| 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, "<text_" + count + ">"); |
| } |
| } |
| // Ensure we have "cl" set |
| if(part.mContentLocation != null) { |
| values.put(Mms.Part.CONTENT_LOCATION, part.mContentLocation); |
| } else { |
| if(part.mPartName != null) { |
| values.put(Mms.Part.CONTENT_LOCATION, part.mPartName + ".txt"); |
| } else { |
| values.put(Mms.Part.CONTENT_LOCATION, "text_" + count + ".txt"); |
| } |
| } |
| |
| if(part.mContentDisposition != null) { |
| values.put(Mms.Part.CONTENT_DISPOSITION, part.mContentDisposition); |
| } |
| values.put(Mms.Part.TEXT, part.getDataAsString()); |
| uri = Uri.parse(Mms.CONTENT_URI + "/" + handle + "/part"); |
| uri = mResolver.insert(uri, values); |
| if(V) Log.v(TAG, "Added TEXT part"); |
| |
| } else if (part.mContentType != null && |
| part.mContentType.toUpperCase().contains("SMIL")){ |
| values.put(Mms.Part.SEQ, -1); |
| values.put(Mms.Part.CONTENT_TYPE, "application/smil"); |
| if(part.mContentId != null) { |
| values.put(Mms.Part.CONTENT_ID, part.mContentId); |
| } else { |
| values.put(Mms.Part.CONTENT_ID, "<smil_" + count + ">"); |
| } |
| if(part.mContentLocation != null) { |
| values.put(Mms.Part.CONTENT_LOCATION, part.mContentLocation); |
| } else { |
| values.put(Mms.Part.CONTENT_LOCATION, "smil_" + count + ".xml"); |
| } |
| |
| if(part.mContentDisposition != null) |
| values.put(Mms.Part.CONTENT_DISPOSITION, part.mContentDisposition); |
| values.put(Mms.Part.FILENAME, "smil.xml"); |
| values.put(Mms.Part.NAME, "smil.xml"); |
| values.put(Mms.Part.TEXT, new String(part.mData, "UTF-8")); |
| |
| uri = Uri.parse(Mms.CONTENT_URI+ "/" + handle + "/part"); |
| uri = mResolver.insert(uri, values); |
| if (V) Log.v(TAG, "Added SMIL part"); |
| |
| }else /*VIDEO/AUDIO/IMAGE*/ { |
| writeMmsDataPart(handle, part, count); |
| if (V) Log.v(TAG, "Added OTHER part"); |
| } |
| if (uri != null){ |
| if (V) Log.v(TAG, "Added part with content-type: " + part.mContentType |
| + " to Uri: " + uri.toString()); |
| } |
| } |
| } |
| } 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, to_address); |
| 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, "<part_" + count + ">"); |
| } |
| } |
| |
| 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<String> parts = smsMng.divideMessage(msgBody); |
| msgInfo.parts = parts.size(); |
| // We add a time stamp to differentiate delivery reports from each other for resent messages |
| msgInfo.timestamp = Calendar.getInstance().getTime().getTime(); |
| msgInfo.partsDelivered = 0; |
| msgInfo.partsSent = 0; |
| |
| ArrayList<PendingIntent> deliveryIntents = new ArrayList<PendingIntent>(msgInfo.parts); |
| ArrayList<PendingIntent> sentIntents = new ArrayList<PendingIntent>(msgInfo.parts); |
| |
| /* We handle the SENT intent in the MAP service, as this object |
| * is destroyed at disconnect, hence if a disconnect occur while sending |
| * a message, there is no intent handler to move the message from outbox |
| * to the correct folder. |
| * The correct solution would be to create a service that will start based on |
| * the intent, if BT is turned off. */ |
| |
| 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++; |
| byte[] pdu = intent.getByteArrayExtra("pdu"); |
| String format = intent.getStringExtra("format"); |
| |
| SmsMessage message = SmsMessage.createFromPdu(pdu, format); |
| if (message == null) { |
| Log.d(TAG, "actionMessageDelivery: Can't get message from pdu"); |
| return; |
| } |
| status = message.getStatus(); |
| if(status != 0/*0 is success*/) { |
| msgInfo.statusDelivered = status; |
| if(D) Log.d(TAG, "msgInfo.statusDelivered = " + status); |
| Sms.moveMessageToFolder(mContext, msgInfo.uri, Sms.MESSAGE_TYPE_FAILED, 0); |
| } else { |
| Sms.moveMessageToFolder(mContext, msgInfo.uri, Sms.MESSAGE_TYPE_SENT, 0); |
| } |
| } |
| if (msgInfo.partsDelivered == msgInfo.parts) { |
| actionMessageDelivery(context, intent, msgInfo); |
| } |
| } 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 == false) { |
| if(D) Log.d(TAG, "actionMessageSent: result OK"); |
| if (msgInfo.transparent == 0) { |
| if (!Sms.moveMessageToFolder(context, msgInfo.uri, |
| Sms.MESSAGE_TYPE_SENT, 0)) { |
| Log.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 == true) { |
| /* 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 */ |
| } |
| } |
| |
| 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 |
| */ |
| static public void actionMmsSent(Context context, Intent intent, int result, |
| Map<Long, Msg> 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); |
| } |
| } |
| |
| static public 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); |
| } |
| } |
| |
| static public 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 == false || |
| msgInfo.sendInProgress == true) { |
| 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 == false) { |
| 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() == false) { |
| // 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; |
| } |
| |
| } |